Modular Flutter Apps — Design and Considerations

Gonçalo Palma
Nov 14, 2019 · 9 min read
Image for post
Image for post
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

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.

Image for post
Image for post
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

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

Creating a mobile SDK

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.

Using the same REST API calls

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.

Sharing Features Between different Applications

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.

Using a Clean-Architecture structure

  • 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

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 Lead Developer @3Rein. Organizer @ Flutter Portugal and collaborator @FlutterExp. https://gpalma.pt/

Flutter Community

Articles and Stories from the Flutter Community

Gonçalo Palma

Written by

Flutter Lead Developer @3Rein. Organizer @ Flutter Portugal and collaborator @FlutterExp. https://gpalma.pt/

Flutter Community

Articles and Stories from the Flutter Community

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store