Flutter Monorepo with Melos: Stop Juggling Repos, Start Building Apps
Because managing 12 separate repositories for one product is not a personality trait. It's a cry for help.
You know that moment when you fix a bug in your shared UI package, bump the version, publish it, then go update the version in your main app, your admin panel, your second app, and that weird internal tool nobody remembers building? And then one of them breaks because someone forgot to update?
Yeah. That moment.
If you've been nodding your head, you either need a monorepo or you need therapy. Probably both. But let's start with the monorepo.
Why I'm Writing This
I currently maintain several Flutter applications — Zomoto (a restaurant POS system running in 3 restaurants), the Arcon Travel App, and the Melbourne Mover driver app. Each of these has shared patterns: common models, similar UI components, overlapping utility code, and Firebase integrations that look almost identical across projects.
Right now, they all live in separate repositories. Every time I improve an error handling pattern in Zomoto, I manually copy it to the other projects. Every time I build a reusable widget, I rebuild it from scratch in the next app. It's not sustainable.
So I'm actively working on migrating these projects into a monorepo structure with Melos. This article is both a guide for you and documentation of my own journey — the setup, the decisions, and the lessons I'm learning along the way.
What Even Is a Monorepo?
Let's strip away the Silicon Valley jargon.
A monorepo is just... putting all your related projects in one single repository. That's it. One repo. Multiple packages. Multiple apps. All living under the same roof.
Imagine you're building a product ecosystem. You've got:
- A mobile app for end users
- An admin dashboard (also Flutter, maybe Flutter Web)
- A shared UI component library
- A shared API client package
- A shared models/entities package
- Maybe a core utilities package
In the traditional approach, each of these lives in its own Git repository. You version them independently, publish them to a private pub server, and pray that version 2.3.1 of your API client is compatible with version 1.8.0 of your models package. Spoiler: it usually isn't at 11 PM on a Friday.
In a monorepo, all of these live together:
taskflow/
├── apps/
│ ├── mobile/ # The main Flutter app
│ └── admin/ # Admin dashboard
├── packages/
│ ├── ui_kit/ # Shared widgets and themes
│ ├── api_client/ # HTTP client, API calls
│ ├── models/ # Shared data models
│ └── core/ # Utilities, extensions, constants
├── melos.yaml # The brain of the operation
└── pubspec.yaml # Root pubspec
Everything is right there. You change a model, and you instantly see if it breaks the mobile app, the admin panel, or both. No publishing. No version bumping. No waiting.
"But Wait, Won't That Be a Mess?"
Great question. And the answer is: yes, if you do it without tooling.
Managing a monorepo manually is like organizing a wedding without a planner. Sure, you can do it. But you'll end up running flutter pub get in 8 different directories, forgetting which packages depend on which, and losing your mind when you need to run tests across everything.
This is where Melos walks in, puts on sunglasses, and says "I got this."
Enter Melos: Your Monorepo's Best Friend
Melos is a tool built by the folks at Invertase (the same team behind FlutterFire). It's designed specifically for managing Dart and Flutter monorepos.
What does it actually do? In plain English:
- Links all your local packages together so they reference each other directly (no publishing needed)
- Runs commands across all packages at once (tests, analysis, code generation)
- Handles versioning and changelogs automatically
- Filters which packages to target based on changes, dependencies, or custom conditions
- Bootstraps everything with a single command
Think of Melos as the project manager your monorepo desperately needs.
Setting Up Melos (The Actual Hands-On Part)
Enough philosophy. Let's build a monorepo from scratch.
Step 1: Install Melos
dart pub global activate melos
That's it. Melos is now available globally on your machine.
Step 2: Create Your Project Structure
mkdir taskflow && cd taskflow
mkdir -p apps/mobile
mkdir -p apps/admin
mkdir -p packages/ui_kit
mkdir -p packages/api_client
mkdir -p packages/models
mkdir -p packages/core
Step 3: Create the Root pubspec.yaml
This is a minimal file. Its job is just to declare that this workspace exists:
name: taskflow_workspace
publish_to: 'none'
environment:
sdk: '>=3.0.0 <4.0.0'
dev_dependencies:
melos: ^6.1.0
Step 4: Create melos.yaml — The Heart of Everything
This file lives at the root and tells Melos how your monorepo is structured:
name: taskflow
packages:
- apps/**
- packages/**
command:
bootstrap:
usePubspecOverrides: true
scripts:
analyze:
run: melos exec -- "dart analyze --fatal-infos"
description: Run dart analyze in all packages
test:
run: melos exec -- "flutter test"
description: Run tests in all packages
packageFilters:
dirExists: test
format:
run: melos exec -- "dart format . --set-exit-if-changed"
description: Check formatting in all packages
build_runner:
run: melos exec -- "dart run build_runner build --delete-conflicting-outputs"
description: Run build_runner in all packages
packageFilters:
dependsOn: build_runner
clean:
run: melos exec -- "flutter clean"
description: Clean all packages
packages tells Melos where to find your packages. The ** glob means "look in all subdirectories."
command.bootstrap.usePubspecOverrides is the magic setting. It tells Melos to use pubspec_overrides.yaml files to link local packages — so when mobile depends on ui_kit, it uses the local version right next to it.
scripts are custom commands you can run across your entire monorepo.
Step 5: Set Up Individual Packages
Each package gets its own pubspec.yaml. Here's what a shared package looks like:
packages/models/pubspec.yaml
name: taskflow_models
description: Shared data models for TaskFlow
publish_to: 'none'
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
freezed_annotation: ^2.4.1
json_annotation: ^4.8.1
dev_dependencies:
build_runner: ^2.4.6
freezed: ^2.4.5
json_serializable: ^6.7.1
And here's how an app references the shared packages:
apps/mobile/pubspec.yaml
name: taskflow_mobile
description: TaskFlow mobile app
publish_to: 'none'
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: '>=3.10.0'
dependencies:
flutter:
sdk: flutter
taskflow_models:
path: ../../packages/models
taskflow_ui_kit:
path: ../../packages/ui_kit
taskflow_api_client:
path: ../../packages/api_client
Notice the path: dependencies. They point to relative paths within the monorepo. No versions. No pub server. Just direct references.
Step 6: Bootstrap
melos bootstrap
Melos will resolve dependencies and link everything together. Your monorepo is alive.
Using Melos Day-to-Day
# Run analysis everywhere
melos run analyze
# Run tests everywhere
melos run test
# Run build_runner where needed
melos run build_runner
# Clean everything
melos run clean
# List all packages
melos list
# See dependency graph
melos list --graph
One command. Every package. No cd-ing around.
Filtering Packages
This is one of Melos's superpowers:
# Only run in packages that depend on freezed
melos exec --depends-on="freezed" -- "dart run build_runner build"
# Ignore specific packages
melos exec --ignore="taskflow_admin" -- "flutter test"
The Architecture: How to Think About Package Design
The Layer Cake
┌─────────────────────────────────────────┐
│ APPS LAYER │
│ mobile / admin / web │
└───────────────┬─────────────────────────┘
│ depends on
┌───────────────▼─────────────────────────┐
│ SHARED PACKAGES │
│ ui_kit / api_client / models │
└───────────────┬─────────────────────────┘
│ depends on
┌───────────────▼─────────────────────────┐
│ CORE PACKAGE │
│ extensions / constants / utilities │
└─────────────────────────────────────────┘
Dependencies only flow downward. An app can depend on shared packages. Shared packages can depend on core. Nothing depends upward.
What Goes Where?
core/ — String extensions, date formatting, custom exceptions, constants, logger setup. Zero Flutter dependency if possible. Pure Dart.
models/ — User model, Task model, API response wrappers, Freezed union types, shared enums.
api_client/ — Dio/http client setup, API endpoint methods, interceptors, error handling.
ui_kit/ — Custom buttons, cards, text fields, app theme, shared widgets, custom animations.
A Real-World Example: Sharing a Widget
// packages/ui_kit/lib/src/task_card.dart
import 'package:flutter/material.dart';
import 'package:taskflow_models/taskflow_models.dart';
class TaskCard extends StatelessWidget {
final Task task;
final VoidCallback? onTap;
const TaskCard({super.key, required this.task, this.onTap});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
title: Text(task.title),
subtitle: Text(task.description),
trailing: _buildPriorityBadge(task.priority),
onTap: onTap,
),
);
}
Widget _buildPriorityBadge(TaskPriority priority) {
final color = switch (priority) {
TaskPriority.high => Colors.red,
TaskPriority.medium => Colors.orange,
TaskPriority.low => Colors.green,
};
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
priority.name.toUpperCase(),
style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.bold),
),
);
}
}
Now in any app:
import 'package:taskflow_ui_kit/taskflow_ui_kit.dart';
// Just use it. No version conflicts. No publishing.
TaskCard(task: task, onTap: () => navigateToDetail(task))
Change it once in ui_kit, and every app gets the update immediately.
Melos + CI/CD: The Power Combo
name: CI
on:
pull_request:
branches: [main]
jobs:
analyze-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
- name: Install Melos
run: dart pub global activate melos
- name: Bootstrap
run: melos bootstrap
- name: Analyze
run: melos run analyze
- name: Run Tests
run: melos run test
Every PR gets analyzed and tested across your entire workspace.
How I'm Applying This to My Own Projects
I'm in the process of migrating my existing Flutter apps — Zomoto, Arcon Travel, and Melbourne Mover — toward a monorepo structure. Here's what I've identified as shared across all three:
- Firebase utilities — Auth setup, Firestore helpers, error handling patterns. Almost identical in all three apps.
- Common UI components — Loading indicators, error screens, custom buttons, status badges. I've rebuilt these from scratch in each project.
- Models and entities — User models, base entity classes, common enums. Copy-pasted with minor variations.
- Networking patterns — Dio interceptors, token refresh logic, API error handling. Same approach, different implementations.
The plan is to extract these into shared packages (core, ui_kit, firebase_utils, networking) and have each app depend on them. The Clean Architecture patterns I already use in Zomoto make this migration natural — the domain and data layers already have clear boundaries.
It's not a weekend project. Migrating existing apps is more involved than starting fresh. But even extracting just the core utilities package has already eliminated duplicate code and made bug fixes propagate instantly.
Common Mistakes (So You Don't Have To Make Them)
Making everything a package. If a piece of code is only used by one app, keep it in that app. Packages are for shared code.
Circular dependencies. Package A depends on B, and B depends on A. Extract the shared code into a third package C.
Forgetting to bootstrap after pulling. Always run melos bs after pulling changes. Make it a habit.
One giant "shared" package. Split it: core for utilities, models for data, ui_kit for widgets, api_client for networking.
Not using publish_to: 'none'. Unless you're publishing to pub.dev, always add this to prevent accidental publishing.
When Should You Use a Monorepo?
Use a monorepo when:
- You have multiple apps sharing code
- Your shared packages change frequently
- You're tired of version conflicts between internal packages
- You want to run tests and analysis across everything in one go
Stick with separate repos when:
- You have a single app with no shared packages
- Your packages are truly independent with different release cycles
- You're building open-source packages for pub.dev
Wrapping Up
Monorepos aren't magic. They don't make bad architecture good. What they do is remove friction. Friction between packages. Friction between projects. Friction between "I fixed the bug" and "every app has the fix."
Melos takes that further by giving you the tools to actually manage the monorepo without losing your sanity. Bootstrap, scripts, filtering, versioning — it handles the boring stuff so you can focus on building.
If you're working on multiple Flutter projects with overlapping code — like I am with Zomoto, Arcon Travel, and Melbourne Mover — give this setup a serious look. The initial setup pays for itself within the first week.
Your future self — the one who doesn't have to manually copy-paste utility code across 4 repositories — will be grateful.
Got questions about setting up your own monorepo? Connect with me on Twitter or LinkedIn. I've made all the mistakes already so you don't have to!