Best Practices for Maintaining and Scaling Flutter Applications
Maintainable and Scalable codebase is arguably the most important consideration when building an app because it is essentially an insurance policy for your code.
When organizations talk about scalable codebase, we often hear that they say,
The app needs to be scalable so that it can handle both the growing number of users and support new features without breaking the existing ones.
But that’s not all! A Flutter app should have a compossible structure that is easy to test, debug and understood by everyone working on it.
Also scalable code ensures that you have the right architecture in place to be able to add new features to an application without breaking the rest of it. It also makes opening pull requests and merging them less scary because you can be sure that your contribution will not introduce new problems. Also teams should be able to scale new features.
Howdy! 👋 My name is Aditya. Currently I’m doing an Internship at FOLK Developers as a frontend developer. While working, I have learnt some helpful techniques that will come handy to you while scaling the codebase for your flutter app or in general to any codebase.
In this article we are not going to talk about what you should do but rather you could do. As each project is different so are the people working on it. So in here we will define abstract standards that you can extend in your project in order to make your codebase truly scalable.
If you still wondering what scalable codebase is or what do you mean by scalable codebase? So here is the definition —
Making your codebase scalable means that if a new feature or requirement come to you, in how much change, you can adapt to that feature without breaking existing one’s. Also the lesser the lines required the more scalable codebase is.
So let’s start with advantages of scalable codebase. Scalable code can help you avoid spaghetti code. Many developers have a horror story about a legacy codebase where deploying any new code meant breaking some other part of the codebase.
So let me ask! Have you ever been in a situation where you just finished implementing an awesome new feature that the user’s will love, but when you start testing it, certainly it starts to break other functionalities of the app, some random unnecessary bugs are jumping around and you desperately try to solve it. Finally in the end when everything is working fine you realize that you spent more time solving the bugs and fixing the functionalities than creating the feature itself.
Now let’s imagine you are in an organization where the developer who has been around since day one has all the knowledge about the codebase but there is no documentation on it. Every time something breaks and you are not able fix it, you will have to hop on a video call with the person and could spend hours tracking down the problem. If that person ever leaves the company, the team loses all of that valuable knowledge and the codebase will be hard to maintain. We’ve probably all encountered a situation like this — or done it ourselves. So in short —
Your codebase will not survive if one person has all of the knowledge in their head. That information needs to be documented and built into the architecture so that every member on the team knows how to build new features in a way that they will not break the existing infrastructure.
Coding Standards
Specifying coding standards throughout the team/organization helps in making the code to be easily testable. If you want to reuse some kind of feature in your module which is already implemented in another module, without coding standards you will not be able to reuse the code easily. It’ll start creating mess and is a potential invitation to unwanted bugs. It will also make testing the code harder then it needs to. So lets start with -
1. Naming Conventions (snake_case)
snake_case is a naming style where all letters in the name are lowercase and it uses underscores to separate words in a name. In addition, in Angular, a dot is used to separate the name, type, and extension for file names. file_name.type.dart
Including the types in the file names make it easy to find a specific file type using a text editor or an IDE.
Most common files types are: .widget, .style, .service, .model, .util, .store
Create additional type names if you must but take care not to create too many.
Examples
file_name.widget.dart
file_name.style.dart
file_name.model.dart
file_name.util.dart
For more information on naming conventions and coding standards, read this.
2. Folder Structure
There shouldn’t be any standard folder structure you should be fallowing for your codebase. But there are set of rules you can follow in order to make you feature set organized.
- You can create specific folders for each and every module of your app in order to separate the development process, it’ll help in encountering the bugs and error’s more accurately and can be tackled before production.
- Inside of lib folder you can create a Core folder to store models, utils, widget components and helpers that can be used inside of any module across the application.
- Inside a specific module, you can create folders for models, core and services to be used inside of specific module and can be easily reusable inside of it.
- Following is the example of a folder structure with above mentioned principles.
3. Decoupling of views
In Flutter decoupling of view stands for segregation of widgets from the main view in order to reuse and reduce number of lines of code.
Flutter views represent the interface presented to the user. They provide interaction and allow the users to feel the essence of the application. Unfortunately, Flutter views are also often ignored, resulting in tightly coupled and non-reusable components. Which you should avoid to reduce the number of lines of code in order to make your code scalable.
Let’s imagine a usecase where you are defining a dialogebox which will show some content according to the callback. You are creating a separate dialogebox for each callback, after sometime you realize that all the dialogeboxes are not responsive according to the design requirements. It doesn’t make sense to edit all the values for all the widgets again and again. If you became a little lazy and specify a dialogebox widget in core of your module then you’ll be able to extend it according to your view’s needs.
They always say that Lazy developers are the best developers which happens to be true in many ways.
So back to decoupling. Decoupling also makes the process of testing and debugging of code easier and faster. Most importantly it’ll reduce the odds of unintentionally creating the bugs which can alter the performance and user experience.
Architecture (Feature based)
Feature is an essential concept to understand in any software design. It’s also used to describe user requirements for software development. Therefore, if we structure our projects by features, it will be easier to manage the codebase when it grows in feature set.
Organize codebase by features
lib
...
feature1
feature2
feature3
feature4
services
utils
managers
As I mentioned above while explaining the folder structure that you can differentiate development process by feature sets. In complex apps, it’s hard to understand how different modules integrate. A feature-oriented architecture is helpful for this because we’ve grouped related logic (widgets|utils|pages|stores|models|..etc)
into features. We don’t need to think about how the small parts work together but how features work together to construct the whole codebase. By analyzing dependencies between features the app could auto-generate understandable diagrams for developers to learn and test the codebase.
Inside of Features
To keep any feature from getting polluted, it’s important to decouple the business logic of that feature from its presentation. That’s why we should split the feature module into two different layers:
- Infrastructure features: maintains services, repositories, models, utils, validators, interfaces, … etc.
- App features: maintains pages, widgets, styles, fonts, colors, …etc.
Steps for creating scalable codebase —
Below we will discuss the four qualities we can strive for while creating scalable code. There is a huge risk associated with relying only on manual testing right before a scheduled release. Oftentimes critical bugs and stability issues surface, resulting in either a poor quality release or a postponed release date.
1. Scalable code is boring
You may think what makes a codebase boring? and how can a boring codebase be good?
This ability to be able to find standard and reproducible patterns in the codebase makes it boring. Producing boring code is the biggest compliment that an engineering team can receive
As Jorge from VVG said,
Having a codebase that is predictable, easy to navigate, well tested and properly automated makes it boring. But pleasantly boring! And that’s great, teams love the feeling! It lets them focus on their real and business-related challenges without the distractions of other tasks.
Codebases should be structured in such a way that any developer of any level — from Junior Developers to Senior Architects — should be able to understand the logic. In short, you should have a boring codebase.
You can achieve the boring codebase by -
- Stick to one state management library which is easy to understand by everyone working on the project. I’m biased about GetX so I prefer using it. Also, at the time of writing, it happens to be the most liked package on pub.dev
- Writing clear documentation is helpful for every person working on the project as it promotes easier flow of work and module requirements.
- You can document the code in such a way that if you were to come back to the code in the future, you would be able to jump right back into it again. It will decrease the debugging time rapidly and help in increasing productivity.
- Your team should be on the same page when it comes to naming variables, functions, and classes. The codebase also should conform to pre-existing standards whenever possible, including following the Effective Dart style guide and using popular packages wherever it makes sense.
2. Try to reach 100% of code coverage in your tests
Testing the written code is foundational part of any development process. If you’re only testing once the app is complete, sorry but you’re doing it wrong! Testing should be baked into the development process and it needs to be validated right after the development.
Untested code is simply unscalable; if some part is left untested, you’re opening up the door to potential bugs or having your app not work the way it was intended.
For larger codebases it’s necessary that your tests reach 100% of code coverage. If your coverage is currently around 95% and your codebase has more than 20000 lines of code, this means that more than 1000 lines of code will left untested — it’s a significant amount!
For a comprehensive resource on testing — you can refer this course
3. The Process of debugging should be easy
If bugs do surface in your codebase, tests you’ve written will likely fail. This can help you isolate the exact part of the code that is causing the issue in a matter of seconds and before the bug impacts end users. If the tests don’t fail and there is still a bug, that’s an indication that once the bug is identified and fixed, a test should immediately be added to ensure that bug never happens again.
Having built-in debuggers and widget inspectors can also make debugging that much easier. Flutter’s built-in tooling provides performance profiling and networking devtools which will help developers spot bugs quicker and then write tests to catch future ones.
4. Scalable code is composable
As we discussed earlier, scalable applications all have this one thing in common: they should be composed of smaller, independent packages or modules. This will ensure that each package has a single responsibility and can be tested and reused. This also ensures that everyone can work on the codebase in parallel without interrupting the work of other members. So you know, let’s go SOLID 😉
TLDR;
You may be wondering that who should be using this? Open source projects, independent developers, startups or Large enterprises — It’s for everyone
Scalable codebases should be an industry standard in flutter community, but unfortunately that’s not the case. Scalable codebase is fundamental requirement for any flutter project which is mainstream or planned to be. As it provides you with assurance that your feature set are well tested and are performing according to your requirement.
If your code coverage is not at 100%, set a goal to raise your current threshold by ten percent within the next couple of weeks. It’s all about implementing good, scalable habits now so that your codebase becomes more and more stable over time.
If you’re worried that your application’s codebase is not scalable, there’s no time like the present to start implementing habits that will put you on track. Unfortunately most people/organizations do choose a more bureaucratic approach and will not jump right into it. But if the project is already live then even if you think you have time and you will make your codebase scalable in near future, that’s not the case. Users will make their first impression on your app as a buggy and useless and will never come to use it again. Probably you are going to loose the initial userbase and your project will not get that strike to go on.
Thank you for reading! Have a good day