Flutter at Scale: Code Sharing using a Monorepo

Aditya Gurjar
7 min readJun 26, 2023

--

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.

Venn Diagram illustrating code sharing between the two apps of a Ticketing Platform

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 thedart_package command for the utilities 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 for bk_ticketing which is a package that depends on the Flutter framework, and bk_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.

apps/booking_app/pubspec.yaml
apps/merchant_app/pubspec.yaml

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,

  1. automatically scan the package folders defined in melos.yaml ,
  2. recursively install all package dependencies,
  3. 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,

  1. 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:

Read My Other Blogs

--

--

Aditya Gurjar

Mobile Engineer. Writing Mostly about Mobile Dev, Mobile DevOps, and a lil bit of life.