13 Local PKG Structure
In Dart, you control the entry point and public exports of a package through the main library file in the lib
directory. This is similar to Python’s __init__.py
but with some key differences.
13.1 Key Concepts
Library File: The main entry point is typically lib/<package_name>.dart
or any file you designate as the public API.
Export Directive: Use export
statements to expose specific symbols from other files, similar to Python’s from .module import something
.
Private Symbols: In Dart, symbols starting with underscore _
are private to the library and won’t be accessible outside.
13.2 Simple Example
Let’s create a local package called my_utils
:
my_utils/
├── lib/
│ ├── my_utils.dart # Main entry point (like __init__.py)
│ ├── src/ # Private implementation details
│ │ ├── math_helpers.dart
│ │ ├── string_helpers.dart
│ │ └── internal_stuff.dart
│ └── models/ # Public models
│ ├── user.dart
│ └── config.dart
├── test/
│ └── my_utils_test.dart
└── pubspec.yaml
13.2.1 File Contents
lib/my_utils.dart (main entry point):
// This is your package's public API - like __init__.py
// Export specific classes/functions from other files
export 'src/math_helpers.dart' show calculateSum, calculateAverage;
export 'src/string_helpers.dart' show capitalize, truncate;
// Export entire files (all public symbols)
export 'models/user.dart';
export 'models/config.dart';
// You can also hide specific symbols
export 'src/string_helpers.dart' hide internalStringFunction;
lib/src/math_helpers.dart:
// Public functions (will be exported)
double calculateSum(List<double> numbers) {
return numbers.reduce((a, b) => a + b);
}
double calculateAverage(List<double> numbers) {
return calculateSum(numbers) / numbers.length;
}
// Private function (won't be accessible even if exported)
double _internalCalculation(double x) {
return x * 2;
}
lib/src/string_helpers.dart:
String capitalize(String text) {
if (text.isEmpty) return text;
return text[0].toUpperCase() + text.substring(1);
}
String truncate(String text, int maxLength) {
if (text.length <= maxLength) return text;
return '${text.substring(0, maxLength)}...';
}
// This won't be exported based on our export statement
String internalStringFunction(String s) {
return s.toLowerCase();
}
lib/models/user.dart:
class User {
final String name;
final int age;
{required this.name, required this.age});
User(}
// Private class - won't be accessible outside
class _UserValidator {
static bool isValid(User user) => user.age > 0;
}
13.3 Using the Package in Flutter
In your Flutter app’s pubspec.yaml
:
dependencies:
my_utils:
path: ../my_utils # Path to your local package
Then in your Flutter code:
import 'package:my_utils/my_utils.dart';
void main() {
// Only exported symbols are available
final sum = calculateSum([1, 2, 3, 4, 5]);
final text = capitalize('hello world');
final user = User(name: 'John', age: 30);
// These would cause errors:
// _internalCalculation(5); // Private function
// internalStringFunction('test'); // Not exported
// _UserValidator.isValid(user); // Private class
}
13.4 Best Practices
Keep
src/
private: Put implementation details inlib/src/
- these files shouldn’t be imported directly by package users.Explicit exports: Be explicit about what you export rather than exporting everything.
Barrel files: You can create multiple “barrel” files for different features:
lib/
├── my_utils.dart # Main entry
├── math.dart # Math-specific exports
├── strings.dart # String-specific exports
└── src/
└── ...
- Documentation: Document your public API in the main library file to help users understand what’s available.
This approach gives you fine-grained control over your package’s public API, similar to Python’s __init__.py
but with Dart’s export system.
13.5 Local Multiple Packages
For a Flutter app that uses multiple local packages exporting widgets, I recommend a clean, modular structure that separates concerns and makes navigation intuitive.
13.5.1 Recommended Directory Structure
Here’s a scalable structure for your Flutter app’s lib/
directory:
my_flutter_app/
├── lib/
│ ├── main.dart
│ ├── app.dart # Main app widget
│ ├── core/ # Core functionality
│ │ ├── constants/
│ │ │ ├── app_colors.dart
│ │ │ └── app_strings.dart
│ │ ├── theme/
│ │ │ └── app_theme.dart
│ │ └── utils/
│ │ └── validators.dart
│ ├── features/ # Feature-based organization
│ │ ├── home/
│ │ │ ├── screens/
│ │ │ │ └── home_screen.dart
│ │ │ └── widgets/
│ │ │ └── home_app_bar.dart
│ │ ├── profile/
│ │ │ ├── screens/
│ │ │ │ └── profile_screen.dart
│ │ │ └── widgets/
│ │ │ └── avatar_widget.dart
│ │ └── settings/
│ │ ├── screens/
│ │ │ └── settings_screen.dart
│ │ └── widgets/
│ │ └── settings_tile.dart
│ ├── shared/ # Shared widgets & components
│ │ ├── widgets/
│ │ │ ├── custom_button.dart
│ │ │ └── loading_indicator.dart
│ │ └── layouts/
│ │ └── main_layout.dart
│ └── packages/ # Wrapper/adapter layer for local packages
│ ├── ui_kit/ # Wrapper for ui_kit package
│ │ └── ui_kit_config.dart
│ └── analytics/ # Wrapper for analytics package
│ └── analytics_setup.dart
├── packages/ # Local packages directory
│ ├── ui_kit/ # Local package 1
│ │ ├── lib/
│ │ │ └── ui_kit.dart
│ │ └── pubspec.yaml
│ ├── analytics_widgets/ # Local package 2
│ │ ├── lib/
│ │ │ └── analytics_widgets.dart
│ │ └── pubspec.yaml
│ └── custom_charts/ # Local package 3
│ ├── lib/
│ │ └── custom_charts.dart
│ └── pubspec.yaml
└── pubspec.yaml
13.5.2 Key Directories Explained
features/
: Organize by feature/module. Each feature has its own screens and widgets:
// lib/features/home/screens/home_screen.dart
import 'package:flutter/material.dart';
import 'package:ui_kit/ui_kit.dart'; // Using local package
class HomeScreen extends StatelessWidget {
@override
{
Widget build(BuildContext context) return Scaffold(
: AppBar(title: Text('Home')),
appBar: Column(
body: [
children// Using widget from local package
PrimaryButton(: 'Click Me',
text: () {},
onPressed,
)// Using app's own widget
,
CustomLoadingIndicator(),
],
)
);}
}
shared/
: Reusable components specific to your app (not from packages):
// lib/shared/widgets/custom_button.dart
import 'package:flutter/material.dart';
import 'package:ui_kit/ui_kit.dart' as ui_kit;
class CustomButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
const CustomButton({
this.text,
required this.onPressed,
required });
@override
{
Widget build(BuildContext context) // Wrapping or extending package widget
return ui_kit.BaseButton(
: Text(text),
child: onPressed,
onPressed: ButtonStyle(
style// Custom styling specific to your app
,
)
);}
}
packages/
: Optional wrapper/configuration layer for local packages:
// lib/packages/ui_kit/ui_kit_config.dart
import 'package:ui_kit/ui_kit.dart';
class UiKitConfig {
static void initialize() {
// Configure package-specific settings
.setDefaultColors(
UiKitTheme: AppColors.primary,
primary: AppColors.secondary,
secondary
);}
}
13.5.3 pubspec.yaml Configuration
name: my_flutter_app
version: 1.0.0
dependencies:
flutter:
sdk: flutter
# Local packages
ui_kit:
path: packages/ui_kit
analytics_widgets:
path: packages/analytics_widgets
custom_charts:
path: packages/custom_charts
# Other dependencies
provider: ^6.0.0
go_router: ^10.0.0
13.5.4 Best Practices
Feature-First Organization: Group screens and their specific widgets together by feature.
Import Management: Create barrel files for cleaner imports:
// lib/features/home/home.dart
export 'screens/home_screen.dart';
export 'widgets/home_app_bar.dart';
- Namespace Imports: Use import aliases to avoid conflicts:
import 'package:ui_kit/ui_kit.dart' as ui_kit;
import 'package:custom_charts/custom_charts.dart' as charts;
// Usage
.PrimaryButton(...)
ui_kit.LineChart(...) charts
- Wrapper Pattern: Create app-specific wrappers around package widgets when you need consistent customization:
// lib/shared/widgets/app_card.dart
import 'package:ui_kit/ui_kit.dart' as ui_kit;
class AppCard extends StatelessWidget {
final Widget child;
const AppCard({required this.child});
@override
{
Widget build(BuildContext context) return ui_kit.BaseCard(
: child,
child: 4.0, // App-specific default
elevation: 12.0, // App-specific default
borderRadius
);}
}
- Documentation: Add README files in key directories to explain the structure:
lib/features/README.md
lib/packages/README.md
This structure scales well as your app grows and makes it easy to: - Find widgets and screens quickly - Understand dependencies - Maintain separation between app-specific code and reusable packages - Add new features without disrupting existing code