Building a Flutter SDK: Breaking Down a Flutter Package — Part One

Deven J.
Deven J.
Published February 13, 2024

Background

In our opinion, it is generally easier to write a package for Flutter compared to other app development frameworks. Packages do not take a tremendous amount of specific knowledge to create if you already know app development in Flutter. In other frameworks, packages can have different structures and several distribution methods, making it much harder to simply start creating packages/libraries.

When our Flutter team started creating an SDK for our first product—Stream Chat (and later Stream Video), it started out as a simple package meant to make it easy to use our backend APIs. However, as time went by, we started adding more functionality to it: UI components, UI builders, offline storage, internationalisation, and more. At the time of writing, the Flutter Chat SDK contained five distinct packages enabling different functionalities. We had to iterate the SDK several times and tried out different architectures, state management, project structures, and so much more.

We realise that the knowledge we’ve gained about building SDKs in Flutter is rather unique. Most of the content out there covers every individual aspect of Flutter. However, since fewer people build SDKs, the know-how gets lost on company Notion pages or stays in the minds of those leading the projects. We hope this set of articles helps you build your SDK in Flutter with relative ease because we believe that some things don’t need to be learned the hard way.

Introduction

We define an SDK as a set of one or more Flutter packages that may work together to achieve a common goal, usually to help integrate a service into a Flutter app. However, before you can understand how to manage a complete SDK, the role of individual parts and files of a Flutter package is critical knowledge to any developer.

This article goes into every aspect of a Flutter package via the files present (or ones that should ideally be added) inside it. We use the Stream Chat Flutter SDK as the main example and most snippets are sourced from the same.

What is a Flutter Package?

A Flutter package is a pre-built collection of code that adds functionality and features to your Flutter app. They act like building blocks, saving you time and effort by letting you reuse existing code instead of writing everything from scratch. These packages can include widgets, libraries, utilities, assets, and more, allowing developers to integrate them into their Flutter projects easily.

There is also a distinction in Flutter between ‘packages’ and ‘plugins’. Packages usually refer to a collection of pure Dart code, while plugins refer to packages with native code inside them. Please note that the word “package” in this series encompasses both packages and plugins unless specifically stated otherwise.

A package can be added to an app through the pubspec.yaml file (seen later) through the dependencies section:

yaml
dependencies:

    stream_chat_flutter: ^7.0.0

Flutter Packages vs. Flutter Apps

The end use of a Flutter package and an app are clearly different. The former is meant to be a reusable code that adds some functionality, while the latter is a complete application that can be compiled for any platform. However, the starter project of an app and a package are similar, almost the same even. Both have the same set of files: the pubspec.yaml for declarations, the lib folder for code, the test folder, and more. The main difference is that even though these files and folders are the same, the use of the files is different.

Understanding Files

Let’s go through the individual files present in a package and understand their uses.

{package_name}.dart

In a normal app, the main.dart file holds the entry point of the app through the main() function. All app execution begins in this function, which is required for any Flutter app. Since a package does not need to start executing independently, it does not need to hold a main() entry point. However, packages contain their own central file with the same name as the package at the root of the lib folder. So, the stream_chat package has a stream_chat.dart file inside lib. So then, what is the point of this central file?

A package contains several classes and files to achieve the functionality that it is meant to perform. However, not all of these classes and methods need to be exposed. Some things may even need to be specifically hidden. The main purpose of the central file is to define what files from the package are exposed to the app. The file may also expose other packages that it imports.

Let’s assume this is our package structure for a hypothetical calculator package: simple_calc

yaml
lib/
├─ simple_calc.dart
├─ calculator.dart
├─ utils/
│  ├─ math_utils.dart
├─ models/
│  ├─ number_model.dart

In this structure, the simple_calc.dart file is the aforementioned central file. We may want to expose the calculator class and the number_model.dart, but not the math_utils.dart. We use the export keyword for exposing files to the app.

For this, the simple_calc.dart file will look like this:

yaml
export 'calculator.dart';
export 'models/number_model.dart';

You do not need to specifically hide anything if you do not want to expose it. All files not exported are hidden by default. You can use the show and hide keywords to only show certain things or hide certain things respectively. For example, let’s say we only want to show the calculator class from the calculator.dart file and hide an ImaginaryNumber class from the number_model.dart.

This is how we would write the mentioned scenario:

yaml
export 'calculator.dart' show Calculator;
export 'models/number_model.dart' hide ImaginaryNumber;

Something to note is that the general convention when creating packages is to add a src folder inside the lib folder that includes everything except the central file. This allows separation between the central file and the package's source code.

The new structure with the same files would be:

yaml
lib/
├─ simple_calc.dart
├─ src/
│  ├─ calculator.dart
│  ├─ utils/
│  │  ├─ math_utils.dart
│  ├─ models/
│  │  ├─ number_model.dart

pubspec.yaml

In general, the pubspec.yaml file in an app is a combination of metadata about a project and the dependencies it imports. The pubspec.yaml belonging to a package is similar to one that belongs to an app but contains different metadata compared to an app and certain added restrictions for dependencies.

Since the file is a YAML file, it contains various properties about the package. Let’s review some of the possible properties you can set in the file. This may not be an exhaustive list; however, these are some of the main properties used when creating a package.

name: Defines the name of the package. This is the name that will show up in pub.dev when a package is published.

yaml
name: stream_chat

version: Defines the version of the package. Flutter allows various kinds of versioning systems, such as a normal version number (”1.0.0”), a patched version (”1.0.0+2”), or even a different tag that can be released as a pre-release(”2.0.0-beta.1” / “2.0.0-dev.1”).

yaml
version: 8.0.0

# or version: 8.0.0+1
# or version: 8.0.0-beta.1

description: Adds a description of the package functionality that is the main description line below the package name on pub.dev.

yaml
description: The official Dart client for Stream Chat, a service for building chat applications.

repository: The repository field, which is not mandatory, includes the URL of your package's source code repository. If you decide to publish your package on pub.dev, the main page will display the repository URL. Although it's not required, we encourage you to furnish either the repository or homepage (or both) as it aids users in comprehending the origin of your package.

yaml
repository: https://github.com/GetStream/stream-chat-flutter

homepage: This URL should link to the website for your package. In the case of hosted packages, this URL is accessible from the package's main page.

yaml
homepage: https://github.com/GetStream/stream-chat-flutter

issue_tracker: The optional issue_tracker field should feature a URL directing to the package's issue tracker, helping users view existing bugs and file new ones. In cases where issue_tracker is absent but the repository is present and directs to GitHub, the pub.dev site defaults to using the issue tracker at the repository issues page.

yaml
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues

documentation: Some packages have a dedicated site for hosting documentation, distinct from the primary homepage and the API reference generated by pub.dev. Include a documentation field containing the respective URL if your package includes supplementary documentation. Pub will display a link to this documentation on your package's main page.

topics: When publishing a package, pub.dev automatically identifies the platforms it supports. In the event that this list is incorrect, use the platforms attribute to explicitly specify the supported platforms. For instance, the following platforms entry ensures that pub.dev lists the package as supporting Android, iOS, Linux, macOS, Web, and Windows:

yaml
platforms:
  android:
  ios:
  linux:
  macos:
  web:
  windows:

publish_to: By default, packages are published to pub.dev. You can use the publish_to to publish your packages to another custom server or stop publishing anything altogether.

yaml
publish_to: none

If you want to publish to your own server, check out this post on the Flutter docs for more information.

LICENSE

The LICENSE file is a crucial component that outlines the licensing terms under which the package's code is distributed and can be used by others. Licensing is fundamental in open-source software development, as it defines how the code can be utilised, modified, and distributed by developers and organisations.

Here are some of the most common licenses used by a Flutter package:

  1. MIT License: This is one of the most permissive licenses. It allows users to do almost anything they want with the code as long as they include the original copyright and license notice in any copy of the software/source.
  2. Apache License: Another permissive license, the Apache License, allows users to use, modify, and distribute the code under certain conditions. It also provides an express grant of patent rights from contributors to users.
  3. BSD License: Like the MIT License, the BSD License is a permissive license that allows for a wide range of uses, but it has a slightly different set of conditions regarding redistribution.
  4. GPL (GNU General Public License): This is a copyleft license, which means that derivative works must be licensed under the same terms. There are different versions of the GPL, such as GPL-2.0 and GPL-3.0, each with its own specific provisions.
  5. LGPL (GNU Lesser General Public License): Similar to the GPL, the LGPL is also a copyleft license but has less stringent library linking requirements.

Note that you can also create your own custom license if you want some part of your code to be proprietary or paid.

README.md

The README.md file in a Flutter package is a primary source of documentation and information for developers interested in using the package. It typically contains essential details about the package, its features, installation instructions, usage examples, and other useful information.

The README is often the only file developers read when judging a package, so be sure to include a solid example of what the package can do and the features that would appeal to prospective users.

CHANGELOG.md

The CHANGELOG.md file in a Flutter project serves as a historical record of all the changes and updates made to the project over time. It helps users stay informed about any new features or improvements, bug fixes, breaking changes, and general version updates.

Here is an example of a few changelog entries:

markdown
## 8.0.0-beta.2

- Bump `photo_manager` dependency to `^3.0.0-dev.5`.
- Updated `stream_chat_flutter_core` dependency to [`8.0.0-beta.2`](https://pub.dev/packages/stream_chat_flutter_core/changelog).

## 8.0.0-beta.1

- Updated minimum supported `SDK` version to Flutter 3.16/Dart 3.2
- Bump `photo_manager` dependency to `^3.0.0-dev.4`.

CONTRIBUTING.md

Most of the Flutter packages published on pub.dev are open-source projects. Like all open-source projects, they often rely on community contributions to keep the project going. To do so, potential contributors need to know more details about the repository, such as any information on the structure package, any steps required to run and test the code, and more. All rules and processes, linter configurations, etc., must be shared across contributors.

Additionally, when an open-source project gets many contributors, it becomes tedious to request the same things from each contributor: documentation, testing, working details, etc. All contributors may also need to sign an agreement if a company owns the package.

To make these scenarios easier to deal with, most large packages add a document to the repository called CONTRIBUTING.md (but you can realistically name it anything as long as it is obvious who it is for) that specifies all the mentioned details.

Here is a snippet from Stream’s [CONTRIBUTING.md](http://CONTRIBUTING.md) file:

yaml
Welcome to Stream’s Flutter repository. 
Thank you for taking the time to contribute to our codebase 🎉.

This document outlines a set of guidelines for contributing to Stream and our packages. 
These are mostly guidelines, not necessarily a fixed set of rules. 
Please use your best judgment and feel free to propose changes to this document in a pull request.

...

View the full file here.

Publishing a Flutter Package

When you finish implementing the functionality within your Flutter package, it’s time to publish.

Before you publish, here are a few steps we (and pub) recommend following:

  1. Format your code with the pubspec formatter (dart format).
  2. Run the pana command to check and analyse anything wrong with your package.
  3. Check you have your LICENSE file set (for your first time uploading), and your CHANGELOG.md and pubspec.yaml are updated with the newest changes.

To publish a Flutter package, you can simply run the following command in your package:

markdown
flutter pub publish

However, before you do, we recommend doing a dry run for your publish by adding the same flag:

markdown
flutter pub publish --dry-run

Note that you can use a package without publishing it to pub.dev - this is done through importing a package from GitHub like so:

markdown
dependencies:
    packageA:
      git:
        url: https://github.com/flutter/packageA.git

Note that the latest commits will be pulled this way, and versioning becomes difficult when using this method.

You can also avoid publishing entirely and use a package from a private repository by using SSH:

markdown
dependencies:
    packageA:
      git:
        url: git@github.com:flutter/packageA.git

Conclusion

Developing a Flutter package can be an easy way to increase the adoption of your service by making it easy to integrate your services into other apps. If you are an individual developer, you can also make it easy for developers worldwide to avoid going through the same troubles you did by exporting some code through a package. In this package, we explored all the bits of a package, from what it means to how it’s structured, what it contains, and even how to publish one.

Articles ahead in the series will explore other more complex aspects of an SDK, such as dealing with architectures, state management, theming, and more. For now, we hope this article gave you more insight into what a Flutter package contains and made building your SDK a tiny bit easier.