Dart Package Tutorial – Getting Started

Learn how to create your first Dart package using test-driven development, generate documentation and publish it to pub.dev. By Agustinus Theodorus.

Login to leave a rating/review
Download materials
Save for later
Share

Learn how to create your first Dart package using test-driven development, generate documentation and publish it to pub.dev.

Often, there are features you want to include in your app, but writing the code can be tedious or difficult. So, you hop on to pub.dev and get the package that can help you add that feature, drastically saving your time and energy.

Packages help you extract the code you need so that it can be reused in the same or multiple apps. Packages are also a way you can contribute to the open source community.

Wouldn’t it be great to create your very own package and share it with the community? With this tutorial, you can! Along the way, you’ll learn how to:

  • Create your first Dart package.
  • Publish your package to pub.dev.
  • Import your package into your Flutter app.

Getting Started

Start by clicking the Download Materials button at the top or bottom of the page to download the starter project.

In this tutorial, you’ll use Visual Studio Code, but you can also continue with Android Studio or IntelliJ IDEA. You’ll focus more on the Dart package development and end with integrating your package into your Flutter app (Android/iOS).

In the example, you’ll use Genderize.io, an Open API for predicting gender based on names.

Open your project in Visual Studio Code or Android Studio, and install your dependencies. In the terminal, type:

cd flutter
flutter pub get

Press Enter, and check the output:

$ cd flutter
$ flutter pub get
Running "flutter pub get" in flutter...                          2,860ms

Build and run your project.

Starter Genderizio project

The app has an input field to enter a name and a button to genderize it. Enter the name Peter and tap Genderize.

Try to genderize Peter name

As you see, you don’t get a result. This functionality is what you’ll implement later using your newly published Dart package.

Understanding Dart Packages

Dart packages are reusable code published on the Dart package registry. They function as libraries for Dart software development. But there are nuances between Dart packages, specifically those of Flutter as plugins.

Dart Package vs. Flutter Plugin

While both are technically Dart packages, a little nuance differentiates them.

As the name implies, Dart packages are made in pure Dart. You can use Dart packages for both Flutter and server-side Dart. Developing a Dart package is easier than a Flutter plugin because you don’t need to test any platform-specific code. Everything is in Dart!

On the other hand, Flutter plugins are pieces of code that function primarily as part of the mobile app. Flutter plugins usually wrap native Android/iOS code in a Dart package to reuse it within the Flutter app. In this tutorial, you’ll make a Dart package, so your package will be reusable within Flutter or simple Dart scripts.

Knowing When to Create a Dart Package

Flutter has a ton of packages for even the slightest of problems.

For example, if you want to create a native splash, Flutter has a native splash page package ready for you to use. Or, if you want to create launcher images, Flutter has a separate package for that too.

However, when you don’t find a suitable package for your needs, it’s usually because:

  • You’re using a brand-new programming language with little community support.
  • The problem is too technically expensive to implement — say, creating a new machine learning library.
  • It’s a common problem that no one has created a plug-and-play solution for yet.

If you’re experiencing the last issue, you’ve found an excellent opportunity to create a new package and provide a solution to the wider community.

Writing Your First Dart Package

Before writing your Dart package, you must understand the API you’ll use. You’ll write a Dart API wrapper for Genderize.io, which predicts gender based on names. Users can submit names and get an approximate probability of that name’s gender.

The API accepts many parameters, but you’ll only use the name parameter.

Use this API directly to see how it works. Tap the link https://api.genderize.io/?name=peter:

{
  "name": "peter",
  "gender": "male",
  "probability": 0.99,
  "count": 165452
}

You see the results of calling this API. Now, you have to create a wrapper around it in Dart.

Creating a Dart Package

It’s finally time to create your first package. Open the terminal in the root of the starter project, and type:

dart create -t package genderizeio

Press Enter, and check the result:

$ dart create -t package genderizeio
Creating genderizeio using template package...

  .gitignore
  analysis_options.yaml
  CHANGELOG.md
  pubspec.yaml
  README.md
  example/genderizeio_example.dart
  lib/genderizeio.dart
  lib/src/genderizeio_base.dart
  test/genderizeio_test.dart

Running pub get...                     1.9s
  Resolving dependencies...
  Changed 46 dependencies!

Created project genderizeio in genderizeio! To get started, run the following commands:

  cd genderizeio
  dart run example/genderizeio_example.dart

You just created the package! This command uses the Dart template and prepares base package files for you. You must fill them with business logic.

The previous command’s output asks you to run a few more commands, so you’ll do that next. In the terminal, type the following commands:

cd genderizeio
dart run example/genderizeio_example.dart

Here is what’s going on in the commands above:

  1. Changed the working directory to your newly created package.
  2. Run the example project.

Press Enter to execute the commands.

$ dart run example/genderizeio_example.dart
awesome: true

You’ve just executed an example project. It has no special code, so you see the simple message awesome: true. You’ll update this file later to run the Genderizeio package.

Understanding Dart Package Project Structure

The package’s core consists of the following files:

  • lib/genderizeio.dart: Main interface file.
  • lib/src/genderizeio_base.dart: Core business logic file. Everything under the lib/src folder is your private implementation and shouldn’t be imported by consumers directly. You have to export all public classes in the lib/genderizeio.dart file.
  • pubspec.yaml: Dependencies and package metadata file.
  • README.md, CHANGELOG.md: Supporting files and documentation.
  • example/genderizeio_example.dart: The example that imports the library as if it were a package and tests whether the app is running.
  • test/genderizeio_test.dart: For testing the core business logic.
Note: The testing file isn’t essential for releasing a new Dart package, but it’s considered good practice to have a set of tests before deploying your package.

Test-Driven Development of the Dart Package

Note: This section is optional because you don’t need to have tests to publish a Dart package. If you’d like to dive right into package implementation, feel free to skip to the Importing Dependencies section, where you’ll find a project ready.

You’ll use the test-driven development (TTD) process to implement your business logic. It means you must first write your tests. After that, you must write your code so that all the tests pass.

Note: If you’re interested in learning more about TDD, check out the books: Android Test-Driven Development by Tutorials and iOS Test-Driven Development.

Since the package is an API wrapper, you’ll only do unit tests.

In testing, you will:

  1. Create a public interface, GenderizeAPI, to use the package.
  2. Add the method Future GenderizeAPI.send(String name) async to call Genderize.io.
  3. Return the object Genderize with the property gender in case of success or throw an exception in case of error.

Replace test/genderizeio_test.dart with the following code:

import 'package:genderizeio/genderizeio.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'package:http/http.dart' as http;

import 'genderizeio_test.mocks.dart';

@GenerateMocks([http.Client])
void main() {
  group('Genderize.io', () {
    // 1
    final client = MockClient();
 
    // 2
    final genderize = GenderizeAPI(client);
    
    test('Peter is male', () async {

      // 3
      when(
        client.get(Uri.parse('https://api.genderize.io?name=peter')),
      ).thenAnswer(
        (_) async => http.Response(
          '{"name":"peter","gender":"male","probability":0.99,"count":165452}',
          200,
        ),
      );

      // 4
      final result = await genderize.send('peter');

      // 5
      expect(result.gender, 'male');
    });
    
    // 6
    test('API exception', () async {
      when(
        client.get(Uri.parse('https://api.genderize.io?name=')),
      ).thenAnswer(
        (_) async => http.Response(
          '{"error":"Missing \'name\' parameter"}',
          500,
        ),
      );
      final result = genderize.send('');
      await expectLater(
        result,
        throwsException,
      );
    });
  });
}

You’ll see several code issues in the editor. That’s because you haven’t added the dependencies you’ve used. You’ll fix the errors soon by adding the dependencies and generating the mock data.

In the above code, you:

  1. Create an instance of mock http.Client. This class has mocked HTTP functions like get and post generated by build_runner.
  2. Create an instance of an API wrapper based on a mocked http client.
  3. Intercepte the network requests to return the mock data in tests.
  4. Call an API Wrapper with “Peter” as the name parameter.
  5. Test if Peter is male, with a result of “male”.
  6. Test to check whether the wrapper returns an exception in case of error.

Next, you’ll start fixing the errors in the above code.

Adding the Dependencies Through Terminal

Open the terminal and navigate genderizeio. Type the following commands to add the dependencies:

dart pub add HTTP
dart pub add build_runner --dev
dart pub add mockito --dev
dart pub get

Press Enter, and check the output.
You’ll see repeated similar messages for each command:

Resolving dependencies...
....
Changed ... dependencies!

This command helps add dependencies directly through the terminal.

Note: You’ll use the Mockito package to mock network responses. It’s bad practice to request remote data in tests. Several issues might give false errors while testing with a real API, like Network Error, Server Error and Access Error. For more on how to use this package, check out this documentation.

Mockito requires you to run build_runner to generate mocks for annotated classes. Look at genderizeio_test.dart and you’ll see @GenerateMocks([http.Client]). Mockito will generate test/genderizeio_test.mocks.dart with a mocked http.Client class.

To generate mocks, run the following command in the terminal:

dart run build_runner build

Press Enter, and check the output:

$ dart run build_runner build
[INFO] Generating build script completed, took 238ms
[INFO] Reading cached asset graph completed, took 26ms
[INFO] Checking for updates since last build completed, took 296ms
[INFO] Running build completed, took 6ms
[INFO] Caching finalized dependency graph completed, took 15ms
[INFO] Succeeded after 27ms with 0 outputs (0 actions)

Create a public interface for your package by replacing lib/src/genderizeio_base.dart with:

import 'package:http/http.dart' as http;


class Genderize {
  Genderize({
    required this.gender,
  });

  final String gender; // The gender prediction
}

class GenderizeAPI {
  GenderizeAPI([http.Client? client]) : client = client ?? http.Client();

  /// Http client dependency to send network requests.
  final http.Client client;

  Future<Genderize> send(String name) async {
    return Genderize(gender: 'unknown');
  }
}

Now all the errors have been fixed

This minimal public interface lets consumers call your package and run tests on it.

Rerun the tests using dart test test/genderizeio_test.dart:

$ dart test test/genderizeio_test.dart 
Building package executable... (2.5s)
Built test:test.
00:00 +0 -1: Genderize.io Peter is male [E]                                                                                                                       
  Expected: 'male'
    Actual: 'unknown'
     Which: is different.
            Expected: male
              Actual: unknown
                      ^
             Differ at offset 0
  
  package:test_api                 expect
  test/genderizeio_test.dart 55:7  main.<fn>.<fn>
  
00:00 +0 -2: Genderize.io API exception [E]                                                                                                                       
  Expected: throws <Instance of 'Exception'>
    Actual: <Instance of 'Future<Genderize>'>
     Which: emitted <Instance of 'Genderize'>
  
  test/genderizeio_test.dart 67:7  main.<fn>.<fn>
  
00:00 +0 -2: Some tests failed.                                                                                                                                   

Consider enabling the flag chain-stack-traces to receive more detailed exceptions.
For example, 'dart test --chain-stack-traces'.

Tests failed, but they were supposed to fail.

You finished the first part of the TTD process. The next sections help you implement business logic so that all the tests can pass.

Importing Dependencies

Note: If you finished the previous section, Test-Driven Development of the Dart Package, you already have all the dependencies that you’ll need. Feel free to go directly to the next section.

Since you need to interact with a REST API, you must add a dependency to communicate with HTTP services.

Open pubspec.yaml, and add the following line in the dependencies section:

dependencies:
  http: ^0.13.4

Open your terminal, and install your Dart dependencies:

dart pub get

Press Enter, and check the result:

$ dart pub get
Resolving dependencies... 
Got dependencies!

The above code adds all the packages mentioned in pubspec.yaml into our project.

Now that you have the HTTP package you can write HTTP functions to get a response from Genderize API.

Dart Package Business Logic

To pass the tests, you must implement the goals you previously defined. Go to lib/src/genderizeio_base.dart and replace its content with:

import 'dart:async';
import 'dart:convert';

import 'package:http/http.dart' as http;

//1
class Genderize {
  Genderize({
    required this.name,
    required this.gender,
    required this.probability,
    required this.count,
  });
  final String name; /// The name submitted through the query parameters
  final String gender; /// The gender prediction
  final double probability; /// The probability of the prediction validity
  final int count; /// The number of names in the database
  
  // 2
  factory Genderize.fromJson(Map<String, dynamic> data) {
    final name = data['name'] as String;
    final gender = data['gender'] as String;
    final probability = data['probability'] as double;
    final count = data['count'] as int;

    return Genderize(
      name: name,
      gender: gender,
      probability: probability,
      count: count,
    );
  }
}

// 3
class GenderizeAPI {
  GenderizeAPI([http.Client? client]) : client = client ?? http.Client();

  /// Http client dependency to send network requests.
  final http.Client client;
  
  // 4
  Future<Genderize> send(String name) async {
    final response = await client.get(Uri.parse('https://api.genderize.io?name=$name'));

    if (response.statusCode == 200) {
      // 5
      final json = jsonDecode(response.body) as Map<String, dynamic>;
      return Genderize.fromJson(json);
    } else {
      // 6
      throw Exception('Failed to load gender');
    }
  }
}

Here’s a code breakdown:

  1. You add a model class named Genderize for the API response.
  2. fromJson returns a Genderize model object.
  3. GenderizeAPI Class is a primary wrapper interface.
  4. A future send(String name) method to call an API which returns a Genderize object or throws an exception if the gender fails to load.
  5. You return the Genderize instance in case of success.
  6. Or throw an exception in case of error.

You fulfilled all your goals for TTD. Now, open the terminal and rerun the tests with the following command:

dart test

Press Enter, and run tests:

$ dart test 
00:01 +2: All tests passed!        

It works! Your Dart package is working well and has passed the tests.

Exporting Your Dart Package

After implementing your Dart project, you need to verify that your package is exportable and importable. lib/genderizeio.dart will be the main entry point to export the project.

Go to lib/genderizeio.dart, and check if your file looks like the code below:

library genderizeio;

export 'src/genderizeio_base.dart';

This file defines that all public variables from src/genderizeio_base.dart are visible to anyone who imports your package using import 'package:genderizeio/genderizeio.dart';.

Now it’s time to check the package you created. You’ll use the example app for this.

Go to example/genderizeio_example.dart, and replace its content with:

import 'package:genderizeio/genderizeio.dart';

void main() async {
  final genderize = GenderizeAPI();
  final result = await genderize.send('peter');
  print('${result.name}: ${result.gender}');
}

In the above code, you:

  • Create a GenderizeAPI instance object named genderize. Now the Genderize methods will be accessible.
  • Call the send function from GenderizeAPI, which takes a name parameter and returns a gender object.
  • Print in the console the genderize object name and gender.

If you’ve done the testing part, you’ll realize this is similar to the unit testing part.

Run the example app to make sure the above code is working.

In the terminal, run the following command to run the example app:

dart run example/genderizeio_example.dart

Press Enter, and check the output:

$ dart run example/genderizeio_example.dart
peter: male

You’ll get the above output. The example app is running and you get an output that tells Peter’s gender.

Publishing Your Dart Package

Your package is almost ready. It only needs a few finishing touches before it goes live. First, you need to create basic documentation to tell developers what your package does and how they can use it.

For simplicity’s sake, change the package’s metadata name to my_genderizeio in pubspec.yaml.

Your pubspec.yaml should look like the screenshot below:

Change the package name in pubspec.yaml

Now you’ll see that your example app and genderizeio_test.dart are throwing errors. This is because you changed the package name and it can’t find the GenderizeAPI class.

To fix this issue, change the import as follow in both files:

import 'package:my_genderizeio/genderizeio.dart';

Creating the Basic Documentation README.md

The documentation for the package shouldn’t be too long and can be straightforward since the package is basically a REST API wrapper. The Dart template fills in the README.md with some usual sections like Getting started and Usage. Go through the file and fill in the sections with information.

Fill in the Usage section with instructions on how developers can use your package:

# Usage
To use the `Genderize.io` package, you can simply import the `GenderizeAPI` class and call the `send` function:
```
final genderize = GenderizeAPI();
final result = await genderize.send('peter');
print('${result.name}: ${result.gender}');
```

README.md instructions are a very important part of the documentation. They tell developers how a package can help them and how to use it.

Next, edit your pubspec.yaml and add a new repository section. You can’t publish the library if it isn’t hosted in a public repository, like GitHub:

repository: https://github.com/your/repository

Your pubspec.yaml should look like this:

Add repository section to pubspec.yaml

LICENSE

To successfully publish your package, you must include a license for your library. The license grants permission to other users and states how they can use the package and under whose name the package is registered. If you aren’t familiar with licenses, copy the MIT license and paste it into the LICENSE file.

Note: Check out https://choosealicense.com/ to choose a proper license for your library.

Generating Package Documentation

Note: This section is optional because you don’t need to have documentation to publish a Dart package. Feel free to skip to the Publishing Package Dry Run section, where you’ll learn how to test your package metadata.

Dart has a very helpful tool for generating documentation using forward slashs. You use three forwards slashes before the function /// and add information to your code. This information will be displayed when the user hovers their cursor over the function.

Comment your lib/src/genderizeio_base.dart to improve the quality of the code. Here’s an example of what it may look like:

Example of documentation

Open Terminal and type the following command:

dart doc

Press Enter, and check the output:

$ dart doc
Documenting my_genderizeio...
Initialized dartdoc with 196 libraries
Generating docs for library genderizeio from package:my_genderizeio/genderizeio.dart...
no issues found
Documented 1 public library in 8.8 seconds
Success! Docs generated into genderizeio/doc/api

Check the project folder; it has a new directory named doc.

Open the index file doc/api/index.html in a browser, and navigate to the send method documentation.

Generated documentation

You’ll now see the documentation for the send method with all comments you added to the code.

Note: To improve your documentation, see the official guide with advanced formatting features.

Importing Your Package Locally

In general, you add the package into your pubspec.yaml and run flutter pub get. This command fetches the package from the online repo and adds it to your project.

You may have noticed that you haven’t deployed your package anywhere online. For the project to fetch the package, you must give the package’s path, or where the package is located locally in your machine.

Go to flutter/pubspec.yaml, and import the package locally by adding the path of the adjacent genderizeio folder:

  my_genderizeio:
    path: ../genderizeio

Open Terminal, change the directory to your Flutter project, and run flutter pub get to verify the installation is correct. Then, check the output:

$ flutter pub get
Running "flutter pub get" in flutter...                            225ms

Go to flutter/main.dart and replace the _predictGender method with the following code:

void _predictGender() async {
  setState(() {
    _gender = _genderLoading;
  });

  final genderize = GenderizeAPI();
  final result = await genderize.send('peter');
  setState(() {
    _gender = 'is probably ${result.gender}';
  });
}

Import the genderizeio package when IDE shows you an error and suggests importing the library.

Build and run the project.

Use package locally in Flutter project

You just used your package in your app!

Publishing a Package Dry Run

Now that you’ve tested your package, it’s ready to go public.

Before you publish your Dart package, do a dry run to test whether everything goes according to plan. Open Terminal, go to the genderizeio folder, and type the following command:

dart pub publish --dry-run

Press Enter, and check the output. Your package should return a message:

$ dart pub publish --dry-run
Publishing my_genderizeio 1.0.0 to https://pub.dartlang.org:
|-- CHANGELOG.md
|-- LICENSE
|-- README.md
|-- analysis_options.yaml
|-- doc
|    -- api
|       |-- __404error.html
|       |-- categories.json
|       |-- genderizeio
|       |   |-- Genderize
|       |   |   |-- Genderize.fromJson.html
|       |   |   |-- Genderize.html
|       |   |   |-- count.html
|       |   |   |-- gender.html
|       |   |   |-- name.html
|       |   |   |-- probability.html
|       |   |-- Genderize-class.html
|       |   |-- GenderizeAPI
|       |   |   |-- GenderizeAPI.html
|       |   |    -- send.html
|       |   |-- GenderizeAPI-class.html
|       |    -- genderizeio-library.html
|       |-- index.html
|       |-- index.json
|       |-- static-assets
|           |-- favicon.png
|           |-- github.css
|           |-- highlight.pack.js
|           |-- play_button.svg
|           |-- readme.md
|           |-- script.js
|           |-- styles.css
|-- example
|    -- genderizeio_example.dart
|-- lib
|   |-- genderizeio.dart
|    -- src
|        -- genderizeio_base.dart
|-- pubspec.yaml
|-- test
     -- genderizeio_test.dart
NullSafetyCompliance.compliant

Package has 0 warnings.
The server may enforce additional checks.

The response looks good, meaning the package is ready to be published.

If your package is missing some files, like the license, it’ll throw an error. So, it’s important that the above command passes without any errors.

Publishing Dart Packages

Now, it’s time to try publishing it for real! Open Terminal, and type the following command:

dart pub publish

Press Enter, and check the output. If everything goes well, you’ll receive:

Publishing my_genderizeio 1.0.0 to https://pub.dartlang.org:
|-- CHANGELOG.md
|-- LICENSE
|-- README.md
|-- analysis_options.yaml
|-- example
|    -- genderizeio_example.dart
|-- doc ...
|-- lib
|   |-- genderizeio.dart
|   |-- src
|        -- genderizeio_base.dart
|-- pubspec.yaml
|-- test
    |-- genderizeio_test.dart
NullSafetyCompliance.compliant

Package has 0 warnings.
The server may enforce additional checks.

Great, you published your first package!

Importing Your Dart Package as Dependencies

To import your package inside your Flutter app, open flutter/pubspec.yaml again and change the dependency to an external one:

my_genderizeio: 1.0.0

Run flutter pub get, and you’ll be good to go!

Build and run your Flutter project.

Flutter project with external package

Where to Go From Here?

You can download the learning materials using the Download Materials button on the top or bottom of the page to compare your results.

Creating a Dart package from scratch is a fairly basic process, and it’s a good learning step to creating and learning Dart overall.

If you’re interested in exploring Dart some more, check out:

I hope you enjoyed making your first Dart package and found this tutorial helpful. Please join the forum discussion below if you have any questions or comments.