Mastering Private Package Management in Flutter: Beyond Git Dependencies
The Flutter news you actually need
No spam, ever. Unsubscribe in one click.
Managing internal packages is a rite of passage for growing Flutter teams. You start with a few shared utilities, then a design system package, then maybe a domain layer—and suddenly you’re wrestling with dependency management instead of building features. Many teams initially reach for hosted private registries, but costs can escalate quickly. The natural fallback—Git dependencies—often introduces its own set of frustrations. Let’s explore the landscape and look at robust strategies that go beyond these basic approaches.
The Problem with Git Dependencies
Using a Git repository as a package source is straightforward in pubspec.yaml:
dependencies:
my_private_package:
git:
url: git@github.com:yourcompany/private_package.git
ref: main
This seems like a free, simple solution. However, you quickly hit limitations:
- Versioning is clunky. You’re stuck with commit SHAs, tags, or branch names. Semantic versioning (
^1.2.0) doesn’t work, making it hard to communicate breaking changes or safe updates. - Dependency resolution suffers. If
package_aandpackage_bboth depend on different commits of the same Git package,pub getcan fail or produce unexpected results. - CI/CD complexity increases. You need to manage SSH keys or tokens for every runner, and fetching multiple repositories slows down your pipeline.
- No discovery. There’s no central list of what packages exist or their available versions.
The core issue is that Git is a source control system, not a package registry. Using it as one forces it into a role it wasn’t designed for.
Leveling Up: Monorepos with Melos
A monorepo consolidates multiple packages into a single repository. This solves many Git-dependency headaches: everything is always at the same commit, cross-package changes are atomic, and setup for new developers is simpler. For Dart/Flutter, Melos is the go-to tool for managing a monorepo.
Here’s a typical melos.yaml:
name: my_company_flutter
packages:
- packages/**
- design_system/
- shared_utils/
scripts:
analyze:
run: flutter analyze
description: Analyze all packages
publish:ci:
run: melos exec -- "if [ -f \"pubspec.yaml\" ]; then flutter pub publish --dry-run; fi"
description: Dry-run publish for all publishable packages
You can then run commands across all packages:
# Install Melos globally
dart pub global activate melos
# Bootstrap the repository (links local packages)
melos bootstrap
# Run tests everywhere
melos run test
The monorepo approach excels at development-time coordination. However, for consuming these packages in other applications (like a production app repo), you still need a distribution method. This is where combining a monorepo with a private registry shines.
The Hybrid Approach: Monorepo + Private Registry
You can enjoy the development benefits of a monorepo while providing a stable, versioned consumption experience for your apps. The workflow looks like this:
- Develop packages together in a Melos monorepo.
- Version and publish selected packages to a private registry.
- Consume them in apps using standard
hosted:dependencies.
First, configure a package for publishing by adding a publish_to field:
# packages/design_system/pubspec.yaml
name: design_system
version: Sixth paragraph removed to eliminate redundancy; the concept was already clearly introduced.
publish_to: https://private-pub.yourcompany.com
Then, use a Melos script to publish only changed packages.
Finally, your application consumes the package like any other hosted dependency, but from your private source:
# app/pubspec.yaml
dependencies:
design_system: ^1.3.0
dependency_overrides:
# Only during local development, if you need to override with a local path
# design_system:
# path: ../monorepo/packages/design_system
To point pub to your private registry, you can set the PUB_HOSTED_URL environment variable.
Choosing Your Strategy
So, what should your team use?
- Git Dependencies: Only for quick, one-off prototypes or when a package is truly coupled to a single app and will never be reused.
- Monorepo (Melos): Ideal when you have multiple interdependent packages worked on by the same team. It streamlines development but doesn’t solve external consumption.
- Private Registry: Necessary for stable, versioned sharing of packages across different teams or applications. Consider self-hosted solutions to control costs.
- Hybrid (Monorepo + Registry): The most robust setup for mature teams. You get the best of both worlds: coordinated development and stable, versioned consumption.
Common Mistakes to Avoid
- Mixing Strategies Inconsistently: Don’t have some apps using Git refs and others using hosted versions of the same package. Standardize.
- Forgetting to Lock Development Dependencies: In a monorepo, use
melos bootstrapto ensure all local packages are linked correctly. - Neglecting Version Semantics: Even with a private registry, follow Semantic Versioning (
major.minor.patch) rigorously. It’s the contract between your package and its consumers.
The goal is to reduce friction, not add it. Start simple, feel the pain points, and evolve your system deliberately. Whether you choose a streamlined monorepo, invest in a private registry, or combine both, the key is to pick a system that lets your team focus on building features, not managing dependency chaos.
This blog is produced with the assistance of AI by a human editor. Learn more
Related Posts
Localizing Dynamic Content in Flutter: A Guide to Backend-Driven Translations
Many Flutter apps need to display content that changes based on user locale, but also comes from a backend (like Firebase). This post will explore best practices for fetching and integrating dynamic, localized content from a backend, ensuring a seamless user experience across different languages and regions without hardcoding translations.
Unraveling Type Mismatch Errors in Flutter: A Guide to 'X can't be assigned to Y' and '_InternalLinkedHashMap' Issues
Developers frequently encounter cryptic type mismatch errors like 'The argument type X can't be assigned to the parameter type Y' or '_InternalLinkedHashMap has no instance method 'cast''. This post will demystify these common Flutter/Dart type errors, explain their root causes (e.g., conflicting imports, dynamic typing pitfalls, JSON deserialization issues), and provide practical solutions to diagnose and fix them, improving code robustness and reducing debugging time.
Solving Flutter Web Memory Leaks: A Practical Guide to Identifying and Fixing Performance Issues
Flutter Web applications can suffer from increasing memory usage over time, leading to performance degradation. This post will delve into common causes of memory leaks in Flutter Web, provide practical debugging techniques using browser developer tools and Dart DevTools, and offer actionable strategies to identify and fix these issues for a smoother user experience.