Custom Widgets & Custom Painting

Packaging Widgets as a Shared Library

16 min Lesson 12 of 12

Packaging Widgets as a Shared Library

As your Flutter projects grow, you will inevitably build reusable widgets that deserve to live in their own standalone package rather than being duplicated across multiple applications. A Dart package lets you extract those widgets into a versioned, independently testable unit with a clean public API. Any Flutter app can then declare it as a dependency and import it just like any pub.dev package.

Note: A Flutter plugin contains platform-specific (native) code, while a Flutter package is pure Dart/Flutter. When packaging UI widgets you almost always create a plain package, not a plugin.

Creating the Package Scaffold

Use the Flutter CLI to generate the boilerplate. The --template=package flag produces a pure-Dart package; --template=plugin would add native channels instead.

Generating a new package

# Create a package called "my_ui_kit" in a sibling directory
flutter create --template=package my_ui_kit

# Resulting structure:
# my_ui_kit/
#   lib/
#     my_ui_kit.dart   <-- main barrel file
#   test/
#   pubspec.yaml
#   README.md
#   CHANGELOG.md
#   LICENSE

Configuring pubspec.yaml

The pubspec.yaml file declares the package identity, SDK constraints, and dependencies. Keep the version following Semantic Versioning (MAJOR.MINOR.PATCH). Set publish_to: none if the package is private and not meant for pub.dev.

Sample pubspec.yaml for a widget library

name: my_ui_kit
description: A shared Flutter widget library for ESB apps.
version: 1.0.0
publish_to: none   # Remove this line to publish to pub.dev

environment:
  sdk: '>=3.0.0 <4.0.0'
  flutter: '>=3.10.0'

dependencies:
  flutter:
    sdk: flutter

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0

flutter:
  uses-material-design: true

Structuring the Library Source Files

Place every widget in its own file under lib/src/. The src/ directory is a convention that signals private implementation details. Files inside src/ are not directly importable by consumers — they must go through the barrel file.

Recommended library layout

lib/
  my_ui_kit.dart          <-- public barrel (the ONLY file consumers import)
  src/
    widgets/
      primary_button.dart
      avatar_badge.dart
      status_chip.dart
    theme/
      app_colors.dart
      app_text_styles.dart
    utils/
      responsive_helper.dart

Writing a Clean Barrel File

The barrel file (lib/my_ui_kit.dart) is the single entry point for consumers. It re-exports every public symbol using export directives. Prefer named show or hide clauses to control exactly which identifiers are visible.

lib/my_ui_kit.dart — the barrel file

/// My UI Kit — shared Flutter widget library.
library my_ui_kit;

export 'src/widgets/primary_button.dart';
export 'src/widgets/avatar_badge.dart';
export 'src/widgets/status_chip.dart' show StatusChip, StatusType;
export 'src/theme/app_colors.dart';
export 'src/theme/app_text_styles.dart';
Tip: Never export src/utils/ helpers unless consumers genuinely need them. Keeping internal utilities unexported lets you refactor freely without breaking the public API.

Consuming the Package in a Flutter App

There are three ways to depend on a local package: a path dependency (for co-located monorepos), a git dependency (points at a specific commit or branch), or a hosted dependency (published to pub.dev or a private registry). Path dependencies are the most common during development.

Referencing the package in the consuming app's pubspec.yaml

dependencies:
  flutter:
    sdk: flutter

  # Local path dependency (monorepo / side-by-side folders)
  my_ui_kit:
    path: ../my_ui_kit

  # OR — git dependency (specific branch)
  # my_ui_kit:
  #   git:
  #     url: https://github.com/yourorg/my_ui_kit.git
  #     ref: main

After updating pubspec.yaml, run flutter pub get. You can now import and use any exported widget with a single import:

Using widgets from the shared library

import 'package:my_ui_kit/my_ui_kit.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          AvatarBadge(
            imageUrl: 'https://example.com/user.jpg',
            isOnline: true,
          ),
          const SizedBox(height: 16),
          StatusChip(status: StatusType.active),
          const SizedBox(height: 24),
          PrimaryButton(
            label: 'Get Started',
            onPressed: () => Navigator.pushNamed(context, '/onboarding'),
          ),
        ],
      ),
    );
  }
}

Versioning and Changelog Discipline

Every time you change the public API, bump the version in pubspec.yaml and add an entry to CHANGELOG.md. Follow Semantic Versioning:

  • PATCH (1.0.0 → 1.0.1) — bug fixes, no API changes
  • MINOR (1.0.0 → 1.1.0) — new backwards-compatible API
  • MAJOR (1.0.0 → 2.0.0) — breaking changes (removed or renamed exports)
Warning: Removing or renaming a symbol that was previously exported is a breaking change. Consumers will get compile errors on their next pub get. Always communicate breaking changes clearly in the CHANGELOG and bump the major version.

Summary

Packaging widgets into a shared Dart library involves four key steps: scaffolding the package with flutter create --template=package, configuring the pubspec.yaml with correct SDK constraints and dependencies, organising source files under lib/src/ with a clean barrel file as the single public entry point, and consuming the package via a path, git, or hosted dependency. Following this pattern produces reusable, independently testable widget libraries that scale gracefully across multiple Flutter applications.

Tutorial Complete!

Congratulations! You have completed all lessons in this tutorial.