Building your own Flutter package from scratch
By John · 1 June 2025
Publishing your own Flutter package lets you share code between your own apps, contribute to the community, or distribute internal tooling. The process from idea to pub.dev takes about an hour for a simple package.
Package vs plugin
- Package: pure Dart/Flutter code, no platform-specific code.
- Plugin: includes native code (Kotlin/Swift). Needed for camera, Bluetooth, file system, etc.
This article covers pure Dart/Flutter packages.
Creating the scaffold
flutter create --template=package my_awesome_package
cd my_awesome_package
Structure:
my_awesome_package/
├── lib/
│ ├── my_awesome_package.dart # Public API barrel file
│ └── src/ # Private implementation
├── test/
├── example/lib/main.dart
├── pubspec.yaml
├── README.md
├── CHANGELOG.md
└── LICENSE
pubspec.yaml
name: my_awesome_package
description: A concise description of what this package does.
version: 0.1.0
repository: https://github.com/yourname/my_awesome_package
environment:
sdk: '>=3.3.0 <4.0.0'
flutter: '>=3.19.0'
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
Public API barrel file
Only export what users should use:
// lib/my_awesome_package.dart
library my_awesome_package;
export 'src/widgets/animated_counter.dart';
export 'src/models/counter_config.dart';
// Do NOT export internal helpers
Writing quality documentation
/// A counter widget that animates number changes.
///
/// ```dart
/// AnimatedCounter(
/// value: 42,
/// duration: Duration(milliseconds: 400),
/// )
/// ```
class AnimatedCounter extends StatefulWidget {
const AnimatedCounter({
super.key,
required this.value,
this.duration = const Duration(milliseconds: 300),
});
/// The integer value to display.
final int value;
/// Duration of the rolling animation.
final Duration duration;
@override
State<AnimatedCounter> createState() => _AnimatedCounterState();
}
Writing tests
Pub.dev shows a coverage indicator. Tests improve search ranking.
testWidgets('displays the value', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(body: AnimatedCounter(value: 42)),
),
);
expect(find.text('42'), findsOneWidget);
});
testWidgets('updates on value change', (tester) async {
int value = 0;
await tester.pumpWidget(
StatefulBuilder(
builder: (_, setState) => MaterialApp(
home: Scaffold(
body: AnimatedCounter(value: value),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => value = 1),
child: const Icon(Icons.add),
),
),
),
),
);
await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();
expect(find.text('1'), findsOneWidget);
});
CHANGELOG.md
## 0.1.0
### Added
- Initial release
- `AnimatedCounter` widget with configurable duration
Publishing
# Check what will be published
flutter pub publish --dry-run
# Publish
flutter pub publish
Pub.dev awards points for: README.md, CHANGELOG.md, API docs on all public members, analysis_options.yaml with lints, tests, and up-to-date SDK constraints.
Semantic versioning
0.0.1 → 0.0.2: bug fix0.0.2 → 0.1.0: new feature (backwards compatible)0.1.0 → 1.0.0: first stable release1.0.0 → 2.0.0: breaking change
CI with GitHub Actions
name: Test
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.22.0'
cache: true
- run: flutter pub get
- run: flutter analyze
- run: flutter test --coverage
Common pitfalls
Exporting internal types. Exported types become public API. Users will depend on them. Only export what you intend to support.
Forgetting the example app. Pub.dev awards points for an example. Show real usage, not just main() {}.
Overly tight SDK constraints. sdk: '>=3.3.0 <3.4.0' blocks users on newer Flutter. Use <4.0.0 unless you have a specific reason.
Publishing a breaking change without a major version bump. Renaming a public class is a breaking change. Bump the major version to signal this.
Sign in to like, dislike, or report.