Gonçalo Palma
November 14, 2019

Modular Flutter Apps — Design and Considerations

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 its 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, we might wonder: can we do the same? And if so, what will we achieve? And 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 sytem to retrieve a library that is currently hosted locally in our machine by referencing its path instead:

dependencies:
	# other dependencies
	
	# our module
	login_module:
		path: ../firebase_login

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 really 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 in order 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 classed for each login that use the Singleton pattern so that we can easily access them.

class FirebaseManager {
  FirebaseManager._();

  static final FirebaseManager instance = FirebaseManager._();

  String login(String username, String password) => "Firebased $username";
}
class ServerManager {
  ServerManager._();

  static final ServerManager instance = ServerManager._();

  String login(String username, String password) => “Servered $username”;
}

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

String _loginWithServer(String email, String password) {
  return ServerManager.instance
      .login(email, password);
}

String _loginWithFirebase(String email, String password) {
  return FirebaseManager
      .login(email, password);
}

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

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:

$ flutter create --org com.vanethos.login firebase_login
$ flutter create --org com.vanethos.login server_login

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:

So, for the firebase_login module we have:

// file: example_login.dart
library example_login;

export 'src/firebase_login.dart';
// file: src/firebase_login.dart

class LoginManager {
  LoginManager._();

  static final LoginManager instance = LoginManager._();

  String login(String username, String password) => "Firebased $username";
}
name: example_login
description: Firebase Login System

version: 1.0.0+1

environment:
  sdk: ">=2.1.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter

And for the server_login we have:

// file: example_login.dart
library example_login;

export 'src/server_login.dart';
// file: src/server_login.dart

class LoginManager {
  LoginManager._();

  static final LoginManager instance = LoginManager._();

  String login(String username, String password) => "Servered $username";
}
name: example_login
description: Custom Backend Login System

version: 1.0.0+1

environment:
  sdk: ">=2.1.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter

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, to we can declare the implementation that we require at any given moment in the pubspec.yaml file of the main project:

name: flutter_modular_app
description: A Flutter app that shows how to approach a modular design

version: 1.0.0+1

environment:
  sdk: ">=2.1.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^0.1.2

  example_login:
    path: ./server_login

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

// file: login_page.dart
import 'package:example_login/example_login.dart';

//... 

String _login() {
  return LoginManager.instance
      .login(_emailTextController.text, _passwordTextController.text);
}

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:

name: flutter_modular_app
description: A Flutter app that shows how to approach a modular design

version: 1.0.0+1

environment:
  sdk: ">=2.1.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^0.1.2

  example_login:
    path: ./firebase_login

In this example, we only used local files, however the same would apply if we have our files hosted in a git repo or even if we are using libraries that are in the pub.dev platform.

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 to 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, 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:

#!/bin/bash

flutter clean

cd android
./gradlew clean
cd ..

START="$(python -c 'import time; print int(round(time.time() * 1000))')"

flutter build apk -t lib/main_dev.dart --flavor dev

END="$(python -c 'import time; print int(round(time.time() * 1000))')"

TOTAL=$(($END-$START))

echo "Total time: $TOTAL"

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 that is has 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.

Creating a mobile SDK

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 give our clients the opportunity 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 exactly 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

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 create a module just for the data layer with the common functionality so that each app can use it as a library.

Using a Clean-Architecture structure

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

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 project with large teams or when we have to ship part of our codebase as a 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.

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:

Follow me!

I often share some small insights on Flutter 💙