← 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 Flutter for High-Performance Desktop: Is it Ready for CAD, Image Processing, and Complex GUIs?

Flutter for High-Performance Desktop: Is it Ready for CAD, Image Processing, and Complex GUIs?

Developers are curious about Flutter's capabilities beyond typical business apps, especially for demanding desktop applications like CAD/CAM or image/video processing. This post will explore Flutter's suitability for high-performance, viewport-based desktop GUIs, discussing Dart's memory model, the 60fps update loop, and real-world examples to gauge its readiness for 'serious' complex software.

Cover image for Debugging Flutter Web Navigation: Solving the Deep Link Refresh Bug

Debugging Flutter Web Navigation: Solving the Deep Link Refresh Bug

Flutter web applications often suffer from a frustrating 'deep link refresh bug' where refreshing the browser on a nested route (e.g., /home/details) bounces the user back to the root or an incorrect path. This post will diagnose the common causes of this issue, explain how Flutter's router handles web URLs, and provide practical solutions and best practices for building robust, refresh-proof navigation in your Flutter web apps.

Cover image for Mastering Internationalization in Flutter: Centralized Strings for Scalable Apps

Mastering Internationalization in Flutter: Centralized Strings for Scalable Apps

As Flutter applications grow, managing strings for multiple languages or just keeping text consistent becomes a challenge. This post will guide developers through effective strategies for centralizing strings, implementing robust internationalization (i18n) and localization (l10n), and leveraging tools to streamline the process for small to large-scale projects.