Flutter at Scale: Code Sharing using a Monorepo
As businesses grow, so do their needs, often leading to larger projects and multiple apps. And as those projects grow in size and number, managing them and sharing code between them can be a real headache.
And this is where monorepos come into play.
What are Monorepos
A monorepo, short for “monolithic repository,” is an approach where multiple projects or modules coexist within a single repository. Rather than having separate repositories for each project, all code is managed together, allowing for easier code sharing, version control, and dependency management.
At scale, monorepos provide several benefits, such as streamlined workflows, consistent development practices, and efficient code reuse.
In general, monorepos can be a vast topic, ranging from things like folder organization to smaller design choices, and for this reason, I’d stick to explaining how you can organize and split your Flutter app(s) into smaller packages.
Tooling We’ll Use
Melos
Melos is the most comprehensive tool for managing packages inside your Flutter monorepo. It comes with features like local package linking, automatic versioning, changelog generation, and lots of other cool stuff.
Very Good CLI
Very Good CLI is an excellent replacement for the default flutter create
command. It comes with some really nice, and opinionated starter templates for generating packages/apps.
Let’s go ahead and install both of these CLIs.
Project Structure
The project structure for our monorepo would look something like this
apps/
, a directory that will contain all the apps.packages/
, a directory that will all the packages that we’ll use in our apps.
Setting up the Workspace
For the workspace root to figure out what version of Melos to use, we need to first add a pubspec.yaml
file defining the Melos version to be used in this workspace.
We then create melos.yaml
which is where we define the config Melos will use to bootstrap the project. Here we’ve defined the package directories. It also lets you define your custom scripts to run inside the workspace root. We’ve kept a simple dart analyze
script for this example.
Overview of the Apps and Packages
A simplified view of how a monorepo can be used to share code between two apps using packages. In this example, we’re considering a Ticketing Platform and the two apps (Booking and Merchant) that it may have.
We are creating four packages here, two of them (ui_kit
and utilities
) will be shared between the apps. The other two are individual features that will only be used by one of the two apps. However, having these features as packages helps us in re-using them in other apps in the future, if the need be.
Creating the Apps
Let’s go ahead and create both apps inside apps/
directory using very_good_cli
.
- It does its thing and creates the apps.
- Now straightaway you can see the template generated here is very different from the one you get with
flutter create
. You get a lot of things baked into the template by default, things that you'd otherwise need to set up manually at some point in the app.
Quoting from the official docs some of my favorite features that come baked into this template:
- Build Flavors: Multiple flavor support for development, staging, and production.
- Internationalization Support: Internationalization support using synthetic code generation to streamline the development process.
- Bloc: Layered architecture
bloc
for scalable, testable code which offers a clear separation between business logic and presentation.- Testing: Unit and widget tests with 100% line coverage.
- Logging: Extensible logging to capture uncaught Dart and Flutter exceptions.
- Very Good Analysis: Stricter Lint rules for Dart and Flutter.
- Continuous Integration: Lint, format, test, and enforce code coverage using GitHub Actions.
Creating the Packages
Creating the packages inside the /packages
directory.
- Notice how we used the
dart_package
command for theutilities
package. This way the created package does not depend on the Flutter framework and is a pure dart-only package. - This is especially useful for creating packages for let's say, the data layer of the apps, or other low-level APIs that may not need access to the Flutter framework.
- Here’s a side-by-side look at the
pubspec.yaml
forbk_ticketing
which is a package that depends on the Flutter framework, andbk_utilities
that doesn’t need it, and thus the additional dependencies.
- The packages use semantic versioning for the version code.
0.1.0+1
here represents a package with a MAJOR version of 0, a MINOR version of 1 (indicating added features or improvements), and a PATCH version of 0 (used to denote bug fixes).
Bootstrapping the Monorepo
We start by adding the packages to both of our app’s pubspec.yaml
. Here’s a look at the dependency map for our monorepo.
We’ll go ahead and add the packages to both apps based on the dependency map above.
Usually, doing a pub get
in the app’s root would install its package dependencies, however, in this case, the packages we’ve added aren’t published on pub.dev, nor have we added a local path reference for them. This means the command simply fails to find the package.
This is where the “melos magic” comes in. Switch to the root of the monorepo and run
This command would,
- automatically scan the package folders defined in
melos.yaml
, - recursively install all package dependencies,
- and locally link the packages together via
pubspec_overrides.yaml
.
Setting up Run Configurations
If you’re using Android Studio, when you bootstrap, melos generates run configurations for all packages and apps in the monorepo.
But as you can notice it doesn’t generate the actual run configurations for our app, instead, it only generates the test run configurations. This is because we’re using flavors and we have main_<flavor>.dart
files as our dart entry points instead of a main.dart
file.
To fix this,
- copy files from your
apps/<app-name>/.idea/runConfigurations
folder to your root./idea/runConfigurations
folder. You can see there are three configurations for each of the flavors we have in our app.
2. Once you’ve copied the run configs for both apps, prefix the file names with the name of the app to make them clear.
3. Update the actual run configuration name and the path to the dart entry point for that specific flavor here. Do this for all the run configurations you’ve just copied.
Save these files and you should be able to see and use the run configurations we just created!
But this is a lot of manual work!
so I instead wrote a shell script to do exactly the steps I explained to create run configurations above and plugged it into Melos, it can be used by simply saying melos generate-run-config
Here’s the script,
Plug it into melos.yaml
And that’s it!
With all your run configurations set up, your monorepo is now ready for use. Next, you can also read up about some additional cool stuff Melos has to offer, like Changelog Generation and Automatic Versioning.
Checkout the complete code at:
Have any questions? Reach out to me on Twitter:
- Or connect with me on Linkedin or Instagram.
- Star the Github repository.
- Follow me on Github.