← Back to posts Cover image for Mastering Private Package Management in Flutter: Beyond Git Dependencies

Mastering Private Package Management in Flutter: Beyond Git Dependencies

· 4 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

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:

  1. 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.
  2. Dependency resolution suffers. If package_a and package_b both depend on different commits of the same Git package, pub get can fail or produce unexpected results.
  3. CI/CD complexity increases. You need to manage SSH keys or tokens for every runner, and fetching multiple repositories slows down your pipeline.
  4. 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:

  1. Develop packages together in a Melos monorepo.
  2. Version and publish selected packages to a private registry.
  3. 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 bootstrap to 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

Cover image for Flutter App Size Optimization: Strategies to Shrink Your APK/IPA Below 10MB

Flutter App Size Optimization: Strategies to Shrink Your APK/IPA Below 10MB

Flutter apps are often criticized for their larger-than-native binary sizes. This post will dive into practical, actionable strategies to significantly reduce your Flutter app's footprint, including build configurations, asset optimization, and dependency management, demonstrating how to achieve app sizes under 10MB.

Cover image for Mastering In-App Subscriptions in Flutter: A Guide to Avoiding Common Pitfalls

Mastering In-App Subscriptions in Flutter: A Guide to Avoiding Common Pitfalls

Implementing in-app subscriptions in Flutter can be surprisingly complex, especially when dealing with cross-platform nuances, receipt validation, and managing different store configurations. This post will delve into the common challenges developers face, compare solutions like RevenueCat with custom implementations, and provide best practices for setting up and maintaining a robust subscription system.

Cover image for Taming Android Emulator RAM: A Developer's Guide to Reducing Memory Usage for Flutter

Taming Android Emulator RAM: A Developer's Guide to Reducing Memory Usage for Flutter

Android emulators are notorious for hogging RAM, often pushing developer machines into swap and slowing down Flutter development. This post will explore the reasons behind high emulator memory consumption and provide practical, actionable strategies and tools (including a custom-built solution) to significantly reduce RAM usage, ensuring a smoother development experience without sacrificing functionality.