Create and Publish a Package in Flutter
Publishing Flutter packages enables you to share reusable code, widgets, and functionality with the global developer community. This guide covers everything from package creation to publication on pub.dev, including best practices for maintainable and widely-adopted packages.
Comprehensive Guide to Creating and Publishing Flutter Packages
Publishing Flutter packages enables you to share reusable code, widgets, and functionality with the global developer community. This guide covers everything from package creation to publication on pub.dev, including best practices for maintainable and widely-adopted packages.
Understanding Package Types
Choose the appropriate package type based on your specific use case and requirements.
Package Categories:
- Dart Package: Contains pure Dart code without Flutter dependencies, suitable for business logic and utilities
- Flutter Plugin: Includes Dart code with platform-specific implementations for Android, iOS, web, or desktop
- Flutter Package: Contains Flutter-specific widgets and components that depend on the Flutter framework
- Federated Plugin: Advanced plugins that support multiple platforms with separate implementations
Step 1: Package Creation and Initialization
Use Flutter CLI to create the appropriate package structure for your needs.
# Create a Dart package (no Flutter dependencies)
flutter create --template=package <package_name>
# Create a Flutter plugin (with platform-specific code)
flutter create --template=plugin --platforms=android,ios,web <plugin_name>
# Create a Flutter package (with Flutter dependencies)
flutter create --template=package <flutter_package_name>
# Create with specific organization
flutter create --org com.example --template=package <package_name>Step 2: Understanding Package Structure
Familiarize yourself with the standard package directory structure and purpose of each component.
<package_name>/
├── lib/ # Main package code
│ ├── src/ # Private implementation
│ │ ├── internal.dart
│ │ └── utilities.dart
│ ├── widgets/ # Public widgets
│ ├── models/ # Data models
│ ├── services/ # Business logic
│ └── <package_name>.dart # Main export file
├── test/ # Unit and widget tests
│ ├── unit/
│ ├── widget/
│ └── <package_name>_test.dart
├── example/ # Example application
│ ├── lib/
│ ├── pubspec.yaml
│ └── README.md
├── assets/ # Package assets (images, fonts)
├── android/ # Android-specific code (plugins only)
├── ios/ # iOS-specific code (plugins only)
├── web/ # Web-specific code
├── pubspec.yaml # Package configuration
├── README.md # Documentation
├── CHANGELOG.md # Version history
├── LICENSE # License file
└── analysis_options.yaml # Static analysis rulesStep 3: Comprehensive pubspec.yaml Configuration
Configure your package with proper metadata, dependencies, and platform support.
name: <package_name>
description: A comprehensive Flutter package that provides <functionality_description>.
version: <initial_version>
repository: https://github.com/<username>/<package_name>
homepage: https://github.com/<username>/<package_name>
issue_tracker: https://github.com/<username>/<package_name>/issues
documentation: https://github.com/<username>/<package_name>/wiki
environment:
sdk: ">=<minimum_sdk_version>"
flutter: ">=<minimum_flutter_version>"
dependencies:
flutter:
sdk: flutter
# External dependencies
http: ^<http_version>
provider: ^<provider_version>
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^<lints_version>
test: ^<test_version>
mockito: ^<mockito_version>
# For Flutter packages
flutter:
uses-material-design: true
# Assets included in the package
assets:
- assets/images/
- assets/fonts/
# For plugins only
plugin:
platforms:
android:
package: com.example.<package_name>
pluginClass: <PluginName>
ios:
pluginClass: <PluginName>
web:
pluginClass: <PluginName>Web
fileName: <package_name>_web.dartStep 4: Implementing Package Core Functionality
Build the main package functionality with proper abstraction and documentation.
// lib/<package_name>.dart - Main export file
library <package_name>;
// Export public APIs
export 'src/<core_class>.dart';
export 'src/<service_class>.dart';
export 'widgets/<main_widget>.dart';
export 'models/<data_model>.dart';
// Package-level constants and utilities
const String packageName = '<PackageName>';
const Version packageVersion = Version(<major>, <minor>, <patch>);
// lib/src/core_class.dart
/// A comprehensive class that provides [FunctionalityDescription]
///
/// Example usage:
/// ```dart
/// final service = CoreClass();
/// final result = await service.performAction();
/// ```
class CoreClass {
final String _apiKey;
final bool _isDebug;
/// Creates a new instance of [CoreClass]
///
/// [apiKey] is required for authentication
/// [isDebug] enables debug logging when true
CoreClass({
required String apiKey,
bool isDebug = false,
}) : _apiKey = apiKey, _isDebug = isDebug;
/// Performs the main action provided by this package
///
/// Returns [Future<String>] with the operation result
/// Throws [PackageException] if the operation fails
Future<String> performAction() async {
try {
// Implementation details
if (_isDebug) {
print('Performing action with API key: $_apiKey');
}
// Simulate async operation
await Future.delayed(const Duration(milliseconds: 100));
return 'Action completed successfully';
} catch (e) {
throw PackageException('Failed to perform action: $e');
}
}
/// Validates the current configuration
bool validateConfiguration() {
return _apiKey.isNotEmpty;
}
}
// Custom exceptions for the package
class PackageException implements Exception {
final String message;
const PackageException(this.message);
@override
String toString() => 'PackageException: $message';
}Step 5: Creating Reusable Widgets
Implement custom widgets that follow Flutter best practices and are easy to use.
// lib/widgets/custom_button.dart
import 'package:flutter/material.dart';
/// A customizable button widget with built-in loading states
///
/// Features:
/// - Multiple button variants (filled, outlined, text)
/// - Loading state with configurable indicator
/// - Customizable colors and typography
/// - Ripple effect and proper touch targets
class CustomButton extends StatefulWidget {
final String text;
final VoidCallback onPressed;
final ButtonVariant variant;
final bool isLoading;
final Color? backgroundColor;
final Color? textColor;
final double borderRadius;
final EdgeInsetsGeometry padding;
final Widget? leadingIcon;
final Widget? trailingIcon;
const CustomButton({
super.key,
required this.text,
required this.onPressed,
this.variant = ButtonVariant.filled,
this.isLoading = false,
this.backgroundColor,
this.textColor,
this.borderRadius = 8.0,
this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
this.leadingIcon,
this.trailingIcon,
});
@override
State<CustomButton> createState() => _CustomButtonState();
}
enum ButtonVariant { filled, outlined, text }
class _CustomButtonState extends State<CustomButton> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Material(
borderRadius: BorderRadius.circular(widget.borderRadius),
color: _getBackgroundColor(theme),
child: InkWell(
onTap: widget.isLoading ? null : widget.onPressed,
borderRadius: BorderRadius.circular(widget.borderRadius),
child: Container(
padding: widget.padding,
decoration: BoxDecoration(
border: widget.variant == ButtonVariant.outlined
? Border.all(color: _getBorderColor(theme))
: null,
borderRadius: BorderRadius.circular(widget.borderRadius),
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.leadingIcon != null && !widget.isLoading)
Padding(
padding: const EdgeInsets.only(right: 8),
child: widget.leadingIcon!,
),
if (widget.isLoading)
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
_getTextColor(theme),
),
),
)
else
Text(
widget.text,
style: theme.textTheme.labelLarge?.copyWith(
color: _getTextColor(theme),
fontWeight: FontWeight.w600,
),
),
if (widget.trailingIcon != null && !widget.isLoading)
Padding(
padding: const EdgeInsets.only(left: 8),
child: widget.trailingIcon!,
),
],
),
),
),
);
}
Color _getBackgroundColor(ThemeData theme) {
if (widget.variant == ButtonVariant.text) return Colors.transparent;
if (widget.variant == ButtonVariant.outlined) return Colors.transparent;
return widget.backgroundColor ?? theme.colorScheme.primary;
}
Color _getTextColor(ThemeData theme) {
if (widget.variant == ButtonVariant.filled) {
return widget.textColor ?? theme.colorScheme.onPrimary;
}
return widget.textColor ?? theme.colorScheme.primary;
}
Color _getBorderColor(ThemeData theme) {
return widget.backgroundColor ?? theme.colorScheme.primary;
}
}Step 6: Comprehensive Testing Strategy
Implement thorough testing to ensure package reliability and maintainability.
// test/unit/core_class_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:<package_name>/<package_name>.dart';
void main() {
group('CoreClass', () {
late CoreClass coreClass;
setUp(() {
coreClass = CoreClass(apiKey: 'test_api_key', isDebug: true);
});
test('should initialize with provided parameters', () {
expect(coreClass, isNotNull);
});
test('validateConfiguration should return true for valid API key', () {
expect(coreClass.validateConfiguration(), isTrue);
});
test('validateConfiguration should return false for empty API key', () {
final invalidCoreClass = CoreClass(apiKey: '');
expect(invalidCoreClass.validateConfiguration(), isFalse);
});
test('performAction should complete successfully', () async {
final result = await coreClass.performAction();
expect(result, 'Action completed successfully');
});
test('performAction should throw PackageException on error', () async {
// This test would mock dependencies to simulate errors
expect(
() async => await coreClass.performAction(),
throwsA(isA<PackageException>()),
);
});
});
}
// test/widgets/custom_button_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:<package_name>/widgets/custom_button.dart';
void main() {
group('CustomButton', () {
testWidgets('should display text when not loading', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CustomButton(
text: 'Test Button',
onPressed: () {},
),
),
),
);
expect(find.text('Test Button'), findsOneWidget);
expect(find.byType(CircularProgressIndicator), findsNothing);
});
testWidgets('should show loading indicator when isLoading is true', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CustomButton(
text: 'Test Button',
onPressed: () {},
isLoading: true,
),
),
),
);
expect(find.text('Test Button'), findsNothing);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
testWidgets('should call onPressed when tapped', (tester) async {
var pressed = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CustomButton(
text: 'Test Button',
onPressed: () => pressed = true,
),
),
),
);
await tester.tap(find.byType(CustomButton));
expect(pressed, isTrue);
});
testWidgets('should not call onPressed when isLoading is true', (tester) async {
var pressed = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: CustomButton(
text: 'Test Button',
onPressed: () => pressed = true,
isLoading: true,
),
),
),
);
await tester.tap(find.byType(CustomButton));
expect(pressed, isFalse);
});
});
}Step 7: Creating an Example Application
Build a comprehensive example app that demonstrates package capabilities.
// example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:<package_name>/<package_name>.dart';
void main() {
runApp(const ExampleApp());
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '<PackageName> Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const ExampleHomePage(),
);
}
}
class ExampleHomePage extends StatefulWidget {
const ExampleHomePage({super.key});
@override
State<ExampleHomePage> createState() => _ExampleHomePageState();
}
class _ExampleHomePageState extends State<ExampleHomePage> {
final CoreClass _service = CoreClass(apiKey: 'example_key', isDebug: true);
String _result = '';
bool _isLoading = false;
Future<void> _performAction() async {
setState(() {
_isLoading = true;
_result = '';
});
try {
final result = await _service.performAction();
setState(() {
_result = result;
});
} catch (e) {
setState(() {
_result = 'Error: $e';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('<PackageName> Example'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Package Demonstration',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
Text(
'This example demonstrates the core functionality of the <PackageName> package.',
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 32),
CustomButton(
text: 'Perform Action',
onPressed: _performAction,
isLoading: _isLoading,
variant: ButtonVariant.filled,
),
const SizedBox(height: 16),
CustomButton(
text: 'Outlined Action',
onPressed: _performAction,
isLoading: _isLoading,
variant: ButtonVariant.outlined,
),
const SizedBox(height: 32),
if (_result.isNotEmpty)
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Result: $_result',
style: Theme.of(context).textTheme.bodyMedium,
),
),
const Spacer(),
Text(
'Package Version: $packageVersion',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
),
],
),
),
);
}
}Step 8: Documentation and README Creation
Create comprehensive documentation that helps users understand and implement your package.
README.md Structure:
- Badges: Pub.dev version, build status, license
- Features: Clear list of package capabilities
- Installation: Step-by-step setup instructions
- Usage: Code examples and implementation guides
- API Reference: Detailed documentation of public APIs
- Examples: Link to example app and code snippets
- Contributing: Guidelines for community contributions
- License: License information and usage rights
Additional Documentation:
- API Documentation: Use dartdoc for automatic documentation generation
- Tutorials: Step-by-step guides for common use cases
- Migration Guides: Help users upgrade between versions
- Troubleshooting: Common issues and solutions
Step 9: Pre-Publication Quality Assurance
Ensure your package meets all quality standards before publication.
# Run static analysis
flutter analyze
# Run all tests
flutter test
# Generate API documentation
dart doc
# Check package health
pana .
# Dry run publication
flutter pub publish --dry-runStep 10: Publication to pub.dev
Publish your package to make it available to the Flutter community.
# Final publication
flutter pub publish
# For packages with platform-specific code
flutter pub publish --dry-run # Always test first
flutter pub publish # Actual publicationPost-Publication Maintenance
Maintain your package with regular updates and community engagement.
Maintenance Checklist:
- Monitor and respond to issues on GitHub
- Review and merge pull requests
- Regularly update dependencies
- Publish new versions with bug fixes and features
- Update documentation with new features
- Engage with the community on discussion forums
- Monitor package health scores on pub.dev
Version Management:
- Follow semantic versioning (MAJOR.MINOR.PATCH)
- Maintain a detailed CHANGELOG.md
- Use git tags for version releases
- Test thoroughly before each release
- Provide migration guides for breaking changes
Best Practices for Package Development
Follow these guidelines to create high-quality, widely-adopted packages.
Code Quality:
- Follow Dart and Flutter style guidelines
- Maintain high test coverage (>80%)
- Use static analysis to catch issues early
- Implement proper error handling
- Write comprehensive documentation
User Experience:
- Provide clear and simple APIs
- Include extensive examples
- Support common use cases out of the box
- Handle edge cases gracefully
- Provide helpful error messages
Community Engagement:
- Respond promptly to issues and questions
- Welcome community contributions
- Maintain transparent development process
- Provide clear contribution guidelines
- Acknowledge contributors in README
By following this comprehensive guide, you'll create professional, maintainable Flutter packages that provide value to the community while establishing your reputation as a skilled Flutter developer and open-source contributor.