← Back to posts Cover image for Beyond Pub.dev: How to Effectively Use Local Flutter Packages in Your Projects

Beyond Pub.dev: How to Effectively Use Local Flutter Packages in Your Projects

· 5 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

Unlock Your Workflow: Mastering Local Packages in Flutter

When building Flutter applications, we often reach for packages on pub.dev. It’s a fantastic resource. But what happens when you need to work with a package that isn’t published there? Perhaps you’re developing a proprietary library for your company, structuring a monorepo, or actively building a package that you want to test in a real app before releasing it. This is where using local packages becomes essential.

The core idea is simple: instead of pointing your app’s pubspec.yaml to a version on a remote server, you point it to a directory on your own machine. However, getting it set up correctly involves a few key steps and awareness of common pitfalls. Let’s walk through the practical process.

The Basic Setup: Path Dependency

The primary tool for this is a path dependency. In your app’s pubspec.yaml, under the dependencies section, you specify the package name and its local file path.

dependencies:
  flutter:
    sdk: flutter

  # Your local package
  my_local_utils:
    path: ../my_local_utils

Here, my_local_utils is the name of the package as defined inside its own pubspec.yaml file. The path is relative to the location of your application’s pubspec.yaml. In this example, the package resides in a directory called my_local_utils, located one level up (in the parent folder).

After adding this, run flutter pub get in your app directory. Flutter will now link to the local package, treating it much like any other dependency.

Step-by-Step: Creating and Linking a Local Package

Let’s create a minimal local package and connect it to an app.

1. Create the package. From your terminal, outside your app directory, run:

flutter create --template=package my_custom_dialog

This creates a new Flutter package project with the necessary structure, including a lib/ folder and its own pubspec.yaml.

2. Define its functionality. Inside my_custom_dialog/lib/my_custom_dialog.dart, add a simple widget.

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: const Text('Success!'),
      content: const Text('Operation completed successfully.'),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: const Text('OK'),
        ),
      ],
    );
  }
}

Ensure the package’s pubspec.yaml correctly declares its name and Flutter SDK constraint.

name: my_custom_dialog
description: A custom dialog package.
version: 0.0.1

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

flutter:
  # This ensures the package is usable for Flutter.

3. Link it to your application. Navigate to your main Flutter application’s directory. Edit its pubspec.yaml.

dependencies:
  flutter:
    sdk: flutter

  my_custom_dialog:
    path: ../my_custom_dialog  # Assuming the package is in a sibling folder

Run flutter pub get. Now you can import and use the widget in your app.

import 'package:flutter/material.dart';
import 'package:my_custom_dialog/my_custom_dialog.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Local Package Test')),
        body: Center(
          child: ElevatedButton(
            onPressed: () {
              showDialog(
                context: context,
                builder: (context) => const CustomSuccessDialog(),
              );
            },
            child: const Text('Show Dialog'),
          ),
        ),
      ),
    );
  }
}

Common Pitfalls and Solutions

  1. Path Errors: The most common issue is an incorrect path. Always use a relative path from your app’s pubspec.yaml to the package’s directory. Double-check the folder structure. Using an absolute path (like /Users/name/projects/package) is possible but less portable.

  2. Package Name Mismatch: The name in your local package’s pubspec.yaml must exactly match the dependency name you use in your app’s pubspec.yaml. If your package is named awesome_tools, your dependency must be awesome_tools, not awesome_tool.

  3. Hot Reload & State: During development, changes in your local package code will not automatically trigger a hot reload in your main app. You need to restart the app (or sometimes run flutter pub get again) for the changes to be picked up. This is a key difference from editing app code directly.

  4. Version Constraints with Path: When using a path dependency, any version field in your app’s dependency declaration is ignored. The version is effectively “whatever is at that path.” It’s good practice to still maintain a sensible version number inside the local package’s own pubspec.yaml.

Advanced Workflow: Monorepos and Multiple Packages

If you’re working with a monorepo (multiple packages and apps in one repository), path dependencies become incredibly clean. You can structure your project like this:

my_monorepo/
├── packages/
│   ├── shared_ui/
│   └── data_client/
├── apps/
│   ├── customer_app/
│   └── admin_app/

Then, in customer_app/pubspec.yaml:

dependencies:
  shared_ui:
    path: ../packages/shared_ui
  data_client:
    path: ../packages/data_client

This keeps everything neatly contained and linked.

When to Publish vs. Keep Local

Local packages are perfect for:

  • Active development: Iterate on a package while simultaneously testing it in an app.
  • Private/proprietary code: Libraries you don’t want to publish publicly.
  • Monorepo architecture: Maintaining tight coupling between related projects.

Once your package is stable and you want to share it broadly, or need version management via pubspec.yaml (e.g., awesome_package: ^2.0.0), then publishing to pub.dev or a private repository server is the next step.

Final Thoughts

Integrating local Flutter packages is a straightforward yet powerful technique. It breaks the dependency on the central pub repository and gives you full control over your development workflow. By correctly using the path dependency and being mindful of the common gotchas, you can seamlessly work with private, in-development, or monorepo packages, making your Flutter project structure more flexible and efficient.

Remember: after any change to the path or the package’s pubspec.yaml, always run flutter pub get in your application to refresh the dependencies.

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Optimizing Flutter UI Performance: Best Practices for Date Formatting and Expensive Operations

Optimizing Flutter UI Performance: Best Practices for Date Formatting and Expensive Operations

Developers often face performance bottlenecks when performing expensive operations like date formatting directly within Flutter's `build` method, especially in fast-scrolling lists. This post will delve into common pitfalls, explain why these operations are costly, and provide practical strategies for optimizing UI performance by caching formatters, using `initState`, and leveraging `compute` for background processing without blocking the UI.

Cover image for Optimizing Your Flutter Dev Setup: IDEs, Simulators, and AI Tools for Peak Productivity

Optimizing Your Flutter Dev Setup: IDEs, Simulators, and AI Tools for Peak Productivity

Flutter developers frequently seek to refine their development environments. This post will dive into popular IDE choices like VS Code and Android Studio, discuss best practices for managing iOS and Android simulators (including in-IDE options), and explore the practical integration of AI tools for code generation and problem-solving to boost overall efficiency.

Cover image for Demystifying Flutter Performance: Practical Strategies for Large-Scale Apps

Demystifying Flutter Performance: Practical Strategies for Large-Scale Apps

Flutter's performance is often blamed for issues in complex applications, but the real culprits are usually architectural decisions, inefficient widget rebuilds, and unoptimized resource handling. This post will dive into common performance bottlenecks in large Flutter apps, providing actionable strategies for profiling, optimizing state management, handling images and network requests efficiently, and leveraging CI/CD for continuous performance monitoring.