Beyond Pub.dev: How to Effectively Use Local Flutter Packages in Your Projects
The Flutter news you actually need
No spam, ever. Unsubscribe in one click.
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
-
Path Errors: The most common issue is an incorrect
path. Always use a relative path from your app’spubspec.yamlto the package’s directory. Double-check the folder structure. Using an absolute path (like/Users/name/projects/package) is possible but less portable. -
Package Name Mismatch: The
namein your local package’spubspec.yamlmust exactly match the dependency name you use in your app’spubspec.yaml. If your package is namedawesome_tools, your dependency must beawesome_tools, notawesome_tool. -
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 getagain) for the changes to be picked up. This is a key difference from editing app code directly. -
Version Constraints with Path: When using a
pathdependency, anyversionfield 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 ownpubspec.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
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.
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.
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.