How to Create Dart Packages for Your Flutter Apps

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 app 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 remote (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

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); }
// bmi_formatter.dart import 'package:intl/intl.dart'; String formattedBMI(double bmi) { final formatter = NumberFormat('###.#'); return formatter.format(bmi); }

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

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; }

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); }

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

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

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';

to this:

import 'package:bmi/bmi.dart';

And voilà! 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); }

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); }
// bmi_formatter.dart part of bmi; String formattedBMI(double bmi) { final formatter = NumberFormat('###.#'); return formatter.format(bmi); }

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';

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

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

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

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

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:

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!

Want More?

Invest in yourself with my high-quality Flutter courses.

Flutter Foundations Course

Flutter Foundations Course

Learn about State Management, App Architecture, Navigation, Testing, and much more by building a Flutter eCommerce app on iOS, Android, and web.

Flutter & Firebase Masterclass

Flutter & Firebase Masterclass

Learn about Firebase Auth, Cloud Firestore, Cloud Functions, Stripe payments, and much more by building a full-stack eCommerce app with Flutter & Firebase.

The Complete Dart Developer Guide

The Complete Dart Developer Guide

Learn Dart Programming in depth. Includes: basic to advanced topics, exercises, and projects. Fully updated to Dart 2.15.

Flutter Animations Masterclass

Flutter Animations Masterclass

Master Flutter animations and build a completely custom habit tracking application.