Modular Flutter Apps — Design and Considerations

Gonçalo Palma
Nov 14 · 9 min read
Photo by Blake Wheeler on Unsplash

When working in a big project in native Android Development, we may suffer a lot from long build times and an incoherent code structure. That’s why many projects tend to divide their codebase into multiple modules. This ensures not only that the project is divided into smaller units that can be independently changed but it also reduces build time since each module can be built separately and, if a module has not been changed, when building the app it won’t be rebuilt. Some apps report a decrease of the total build time from 1 minute and 10 seconds to 17 seconds.

In the Flutter verse, each time that we compile our code, all the dependencies are also compiled, so we might think that the compile times shouldn’t be affected, however, are there other benefits to do this? Is it worth it? Let’s explore it.

Dividing a project into different modules

Though we do not have the Gradle system that let us easily divide the project into different modules, we can take advantage of the pub package system. How? We can add a new dependency to our project by adding it to the pubspec.yaml file, where it is going to be downloaded from pub.dev. However, we can also use this system to retrieve a library that is currently hosted locally in our machine by referencing its path instead:

This way, we can both easily add new modules and replace a module with another just by changing the path that it points to.

But what practical use cases can we have for this feature? Let’s take an example app that has a login page and a home page.

To the user, it does not matter if this app does the login via Firebase or via API calls to a backend server since she will always have to put her email and password to access the home page. However the same does not apply to the codebase, since the code needed to login with the firebase_auth is going to be widely different than the code we use to login via REST API calls using dio or http. Thus, if we want to isolate the logic of each of these types of login, we may create a Manager class that will login the user given the username and password.

To keep things simple, let us assume that a login via firebase returns a Firebased $username String and a login via the backend returns a Servered $username String. Furthermore, since we do not want to put this logic in the UI dart file, we create separe files and separate classes for each login that use the Singleton pattern so that we can easily access them.

To use these either of these methods in our Widget, we can use one of the following:

And though both methods have the same arguments and the same return type, we will have:

  • One single method called _login and one import at the top of the file. If we want to change from one implementation to another we will have to change the import and the body of the _login method.
  • Two methods, as seen above, called _loginWithServer and _loginWithFirebase and the two imports. To change the implementation from one to another we will have to go to the UI code and change the method being called.

In both situations, there is a cost of changing from one implementation to another. Moreover, the UI does not need to be aware if we are using Firebase or a backend system, so what if we could always use the same import and the same method names? We can do that by creating two different modules and adding them as libraries in our pubspec.yaml file.

To do that, we start by creating each module via the command line:

In our project, two new flutter modules have been added to the project.

Project Structrure after adding both modules

Then, for each of the modules we will do the following:

  • Copy the necessary code for the library and place it in the /src folder
  • Create a login.dart file where we expose the module as a library and includes an export statement to the file in the /src directory.

So, for the firebase_login module we have:

And for the server_login we have:

With the files having the same project name, class name and library name, with the only differences being the filename in the src folder and the actual business logic, the UI will only have to include 1 import to be able to use these libraries, as we have stated earlier. So, effectively, we can declare the implementation that we require at any given moment in the pubspec.yaml file of the main project:

This will ensure that our UI will use the following method to login:

To change the implementation from server_login to firebase_login, we would just have to change the path we are currently assigning in the pubspec.yaml file:

In this example, we only used local files, however the same would apply if we have our files hosted in private git repo.

Benefits of modularisation

As we saw, if we construct our modules in a specific way, we can easily “plug-in” a new module to be used in our app without having to change our codebase. So we can easily change from a login system to another or from a database implementation to another and so on.

Additionally, it will make us look at our code differently. Since we are not able to put all our business logic in the login_page.dart file where we have all the widgets, we will have to create small units of code to manage a specific set of features of our app. This does not only have the benefit of making our code easier to understand, but it also makes our code easier to test (as a side-effect), since we can easily create a test implementation for the LoginManager or we can mock it with mockito to have control over what each function should return.

Finally, we might wonder if we have the same added benefits as we see in Android Development in terms of compile times, and so we test it.

To test it, a 40+k line of code application was divided into 5 different modules and then it was tested the time that it takes to build the android app in both situations via the following bash script:

In the end, after testing 5 builds for the modular and non-modular app, we see that the modular app has a near-zero increase of build time — 0.59 seconds. This is due to the fact that Dart compiles every dependency for each run and as such we cannot have built modules as we have in the Native Android world.

Real-World applications for modularisation

Though we do like to have cleaner code, it is not often that we need to change an implementation back-and-forth from firebase login to server login, so are there use cases for this approach? Let us explore some possible applications for this approach.

At the company I currently work on, we have an Identity Provider mobile app that can manage logins in a website via our service. One of our objectives was to allow our clients to integrate part of our code into their apps so that they could use our login feature without the need for the users to download a new app.

From the development point-of-view, this is almost the same code that was used in the mobile app, so we could easily copy-and-paste-it into a new project and the case was solved.

However, what if we needed to change how we authenticate users into our system? Then we had two different code bases to change code in. The solution to this case was to divide the app into different modules, for example, a login module that can easily be used by the customers and by our app.

We may have to manage a handful of apps that all use the same API calls and data structure. Imagine a school that has an app that has different UI for students and teachers, with different data and functionality being shown in each case, but at the core, it has 80% of the same API calls and data structures.

In this case, it is best to think of creating a module just for the data layer with the common functionality so that each app can use it as a library.

In some applications sharing the REST API calls layer may not be sufficient, so we might need to create different packages that enclose a specific feature for the app.

This may be in a form of UI, for example a login screen and the underlying business logic, or in terms of business logic, such as the encryption model described earlier. If the modules are created in a way that they don’t have a dependency on each-other, we can easily create a new app by grouping different sets of features from this module library.

We can also apply some of the fundaments of the Clean Architecture and divide our app into three separate layers:

  • Data — in this layer we have all the API calls to the server and respective models, remote and all the shared-preferences and databases, local.
  • UI — contains all classes that are used to show information to the user, widgets, BLoCs, utilitarian classes for UI-related code, the MaterialApp or CuppertinoApp class, dimensions, assets paths, and UI-only models.
  • Domain — this layer will be the bridge between the Data and the UI. Since it has a dependency on Data, it will call the necessary method to retrieve data from the API or database and map it in a way that is readable by the UI. Additionally, it will hold any business logic needed to manage the app. This layer only exposes methods to the UI, and it does not have any dependency on the UI.

By building a modular app where we create a data, domain and ui modules, we can assure that for example, the data layer will not have any reference to the UI or the domain since it does not have any dependency to it.

This will also make it easier for teams to work in different parts of the project at the same time. Since the domain knows that the method getBooks will result in a List<Book>, the developers working on the data layer can easily change the URL endpoints or the implementation from http to dio without any problem if they can always output the same List<Book> return type for the getBooks method.

Conclusion

Though there are some real-world scenarios where dividing a Flutter app into different modules has an added benefit, it is a process that can be quite difficult to implement in a relatively-large codebase that does not have decoupled code or dependency injection.

It is also a task that requires a lot of planning since it is easy to create several small modules that may lead to dependency issues and a lot of headaches in the long run.

However, it does force us to rethink the way that we build our apps and it is a good solution when working in a project with large teams or when we have to ship part of our codebase as an SDK.

In the end, this will depend on the circumstances of each project. If we are working in a small app where we make a handful of API requests to one server it might be the best option, but if we have a common codebase that is used by 20 apps in our company, it may be the best approach to developing those apps.

Finally, as suggested by Miguel Medeiros, we can go a step further in terms of creating modules for our app by creating that provides the interfaces to be used in each feature. This way, each time a developer has to create a new login module with a 3rd party service, he will be forced to follow the imposed interface rules, so we are guaranteed to always have the same arguments and return types for that methods. This approach is also used by the Flutter team in their plugins packages: the Federated Approach

What are your thoughts? Do you think that your codebase would improve if you divided it into modules?

The code for the example given in this article is hosted in Github:

  • Non-modular approach:
  • Modular approach:

Flutter Community

Articles and Stories from the Flutter Community

Thanks to miguel medeiros and Nash

Gonçalo Palma

Written by

Flutter developer and enthusiast. Organizer @ Flutter Portugal and collaborator @ FlutterExp. https://gpalma.pt/

Flutter Community

Articles and Stories from the Flutter Community

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade