Monorepo
monorepo is a repository structure where multiple applications and shared packages coexist in a single repository. This setup offers easy code sharing and dependency management between apps, especially for large codebases where multiple projects rely on the same packages or modules.In a Flutter monorepo:- Apps can be managed independently within the same repository.
- Common packages (e.g., shared UI components or utilities) are accessible to all apps without duplicating code.
Monorepo using melos
Melos is a powerful CLI tool specifically designed for managing Dart and Flutter projects in a monorepo. It simplifies dependency management, package linking, and versioning, all while offering efficient commands to manage multiple projects and packages.Benefits of Using Melos:
- Simplifies package interdependencies and linking within the monorepo.
- Provides automated versioning and changelog generation.
- Supports running commands and tests across multiple packages at once.
Setting Up Monorepo Project Structure:
- apps
- app1
- libs
- main.dart
- pubspec.yaml
- app2
- libs
- main.dart
- pubspec.yaml
- packages
- authentication
- libs
- pubspec.yaml
- products
- libs
- pubspec.yaml
- melos.yaml
- pubspec.yaml
Installing and Configuring Melos:
To start, install Melos via the Dart CLI:dart pub global activate melos
name: my_flutter_monorepo
packages:
- "apps/*"
- "packages/*"
flutter pub add melos
Linking Packages to Apps Using Melos
With Melos, linking shared packages to apps is straightforward.Add Shared Packages as Dependencies:
In app1’s pubspec.yaml, add your shared package as a dependency:dependencies:
authentication:
products:
Install Dependencies:
Run the following command to install dependencies across all apps and packages:melos bootstrap
Conclusion
A Flutter monorepo, managed with Melos, brings organization, efficiency, and scalability to your projects, making it easy to share packages across multiple applications. With Melos commands, you can manage dependencies, link packages, and automate workflows, helping to maintain consistency across projects in a single codebase.Flutter Monorepo Drawbacks
While developing our CLI, we noticed some issues that you might encounter:
- Objectbox:
Depending on objectbox FAQs:
The ObjectBox generator only looks for entities in the current package, it does not search workspace packages. However, you can have a separate database (MyObjectBox file) for each package. Just make sure to pass different database names when building your BoxStore.Scenario:
If you have two separate packages (e.g., “categories” and “products”), and the Product model includes a reference to Category (with the Category model located in the “categories” package),
the ObjectBox generator will throw an error in the “products” package because it cannot access the Category model.Solution:
To resolve this, you need to duplicate the Category model in the “products” package. Once you do this, the ObjectBox generator will successfully generate the Product BoxStore. - Injectable:
The Injectable generator only scans for dependencies within the current package and does not look into other workspace packages. This limitation can lead to errors if a dependency is injected in a different package.Scenario:
Each Retrofit remote data source relies on a Dio instance for network requests. Generally, a Dio instance is registered as a lazySingleton in a CoreModule within the core package of the monorepo.
For example, if you have a products package with a Retrofit remote data source that depends on Dio, the Injectable generator will throw an error because it cannot find the Dio configuration within the products package, as it’s unaware of the CoreModule configuration in the core package.@module abstract class CoreModule { @lazySingleton Dio provideDio(AppEnvironment environment) { final dio = Dio() ..transformer = BackgroundTransformer() ..options = BaseOptions( baseUrl: '${environment.baseUrl}/api', ) ..interceptors.addAll([HttpInterceptor()]); return dio; } @test AppEnvironment get testEnvironment => TestEnvironment(); @dev AppEnvironment get developmentEnvironment => DevelopmentEnvironment(); @prod AppEnvironment get productionEnvironment => ProductionEnvironment(); }
Solutions:
Case 1: Isolated Configuration for Different Packages
If the products package needs a separate Dio instance with a different configuration (e.g., connecting to a different backend), you can declare a @Named Dio instance in the products package itself.
This registers an isolated instance for the products package, ensuring each products remote data source uses this named instance.@module abstract class ProductsModule { @lazySingleton @Named('products') Dio provideDio() { final dio = Dio() ..transformer = BackgroundTransformer() ..options = BaseOptions( baseUrl: 'https://www.productsDomain.com/api', ); return dio; } }
@LazySingleton(scope: 'products') @RestApi() abstract class ProductsRemoteDataSource { @factoryMethod factory ProductsRemoteDataSource(@Named('products') Dio dio) = _ProductsRemoteDataSource; }
Case 2: Shared Dio Instance Across Packages
If all packages connect to the same backend and use the same base URL, you can register the Dio instance in one package (e.g., the core package), and then configure other packages to skip over missing instances by using ignoreUnregisteredTypes.Ensure that in your app’s main.dart, you call configureCoreDependencies before configureProductsDependencies:@InjectableInit(ignoreUnregisteredTypes: [ Dio, // Ignoring missing Dio config in the products package ]) Future<void> configureProductsDependencies({Environment? environment}) async { await GetIt.instance.init(environment: environment?.name); }
void main() async { WidgetsFlutterBinding.ensureInitialized(); // ... // Injecting dependencies in the correct order const environment = Environment(Environment.dev); await configureCoreDependencies(environment: environment); await configureProductsDependencies(environment: environment); await configureAppDependencies(environment: environment); runApp(const MyApp()); }