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;
  
  User({required this.name, required this.age});
}

// 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

  1. Keep src/ private: Put implementation details in lib/src/ - these files shouldn’t be imported directly by package users.

  2. Explicit exports: Be explicit about what you export rather than exporting everything.

  3. 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/
    └── ...
  1. 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.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: AppBar(title: Text('Home')),
      body: Column(
        children: [
          // Using widget from local package
          PrimaryButton(
            text: 'Click Me',
            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({
    required this.text,
    required this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    // Wrapping or extending package widget
    return ui_kit.BaseButton(
      child: Text(text),
      onPressed: onPressed,
      style: ButtonStyle(
        // 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
    UiKitTheme.setDefaultColors(
      primary: AppColors.primary,
      secondary: AppColors.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

  1. Feature-First Organization: Group screens and their specific widgets together by feature.

  2. Import Management: Create barrel files for cleaner imports:

// lib/features/home/home.dart
export 'screens/home_screen.dart';
export 'widgets/home_app_bar.dart';
  1. 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
ui_kit.PrimaryButton(...)
charts.LineChart(...)
  1. 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,
      elevation: 4.0,  // App-specific default
      borderRadius: 12.0,  // App-specific default
    );
  }
}
  1. 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