After two years of Fluttering, I would like to share in this article the best practices that I’ve learned of how to build a scalable application.
I will not say what you should do, but what you could do. This guideline will make sure you, and anyone else maintaining the application, can find anything you’re looking for easily and intuitively.
That said, let’s discuss how you can achieve that.
1) Architecture: Feature-based
Feature is an essential concept to understand 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 project when it grows as we construct the system with bigger units.
Organize project by features
In complex apps, it’s hard to understand how different modules collaborate. 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 app. By analyzing dependencies between features the app could auto-generate understandable diagrams for developers to learn or review the project.
Features types
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 app into two different layers:
Infrastructure features: contains all the features that are responsible for implementing the business logic of the application (e.g: auth, http, config, user, articles, events, schools, …etc.)
App features: contains all the features that are responsible for implementing the presentation of the application (e.g: auth, home, settings, user, articles, events, schools, …etc.)
Notice that auth, user, events, articles, …etc. features can be both infrastructure and app features, so what is the difference? that’s what we will discuss in the next section (Features anatomy).
Features anatomy
- Infrastructure features: maintains services, repositories, models, dtos, utils, interceptors, validators, interfaces, …etc
- App features: maintains pages, widgets, styles, fonts, colors, …etc.
Note: An app feature may consume multiple Infrastructure features
2) Naming conventions: Naming files
Snake case (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 type 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
3) State management: Provider + MVVM
State management is a complex topic in Flutter. Each State Management approach has its characteristics and each person has different preferences. For me, Provider was the best choice because it is easy to understand and it doesn’t use much code.
That said, Provider itself isn’t enough to build scalable apps, so I ended up building my package for state management that combines both Provider and MVVM features and called it PMVVM.
P.MVVM
In PMVVM we have 3 major pieces are needed, everything else is up to you. These pieces are:
-
View: It represents the UI of the application devoid of any Application Logic. The
ViewModel
sends notifications to theview
to update the UI whenever state changes. -
ViewModel: It acts as a bridge between the
Model
and theView
. It’s responsible for transforming the data from theModel
, it also holds the events of theView
- Model: Holds app data and the business logic. It consists of the business logic - local and remote data source, model classes, repository. They’re usually simple classes.
Advantages ✔️
- Your code is even more easily testable.
- Your code is further decoupled (the biggest advantage.)
- The package structure is even easier to navigate.
- The project is even easier to maintain.
- Your team can add new features even more quickly.
When to use it 👌
To keep it simple, use the MVVM
whenever your widget has its own events that can mutate the state directly e.g: pages, posts, ..etc.
Some Notes
-
View
can't access theModel
directly -
View
is devoid of any application logic -
ViewModel
can have more than oneView
.
Usage
1. Build your ViewModel
.
class MyViewModel extends ViewModel {
int counter = 0;
// Optional
@override
void init() {
// It's called after the ViewModel is constructed
}
// Optional
@override
void onBuild() {
// It's called everytime the view is rebuilt
}
void increase() {
counter++;
notifyListeners();
}
}
You can also access the context
inside the ViewModel
directly
class MyViewModel extends ViewModel {
@override
void init() {
var height = MediaQuery.of(context).size.height;
}
}
2. Declare MVVM
inside your widget.
class MyWidget extends StatelessWidget {
const MyWidget({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MVVM<MyViewModel>(
view: (context, vmodel) => _MyView(),
viewModel: MyViewModel(),
);
}
}
3. Build your View
.
class _MyView extends StatelessView<MyViewModel> {
/// Set [reactive] to [false] if you don't want the view to listen to the ViewModel.
/// It's [true] by default.
const _MyView({Key key}) : super(key: key, reactive: true);
@override
Widget render(context, vmodel) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(vmodel.counter.toString()),
SizedBox(height: 24),
RaisedButton(onPressed: vmodel.increase, child: Text('Increase')),
],
);
}
For more details, head to the package documentation
P.MVVM for Web, Mobile, and Desktop together
pmvvm
works perfectly especially if your app runs on multiple platforms. All you need is to create a single view model that controls all these views:
4) Styling
In Flutter, we often make files for colors, strings, text styles, themes. This way all of these values are kept in one, easy to find a place that should make life easier for the person who gets stuck with maintaining the app.
Styling as a feature
We should group app-wide
colors, fonts, themes, and animations as an app feature called styles
. This approach will make all the widgets in the application consume the styles from a single source.
Example:
colors.style.dart
abstract class CColors {
static const white0 = Color(0xffffffff);
static const black100 = Color(0xff000000);
static const blue10 = Color(0xffedf5ff);
static const blue20 = Color(0xffd0e2ff);
static const blue30 = Color(0xffa6c8ff);
}
text.style.dart
abstract class CFonts {
static const primaryRegular = 'IBMPlexSans-Regular';
static const primaryLight = 'IBMPlexSans-Light';
static const primaryMedium = 'IBMPlexSans-Medium';
static const primarySemibold = 'IBMPlexSans-SemiBold';
static const primaryBold = 'IBMPlexSans-Bold';
}
More examples can be found Here
Widgets styling
If your widget is complex and has some reactive behavior based on specific actions (e.g: background color changes when a button is tapped), then you probably need to separate your widget colors and layout variables from the widget code.
Example:
tile.style.dart
abstract class TileStyle {
static const Map<String, dynamic> layouts = {
'tile-padding': const EdgeInsets.all(16),
};
static const Map<String, Color> colors = {
'tile-enabled-background-color': CColors.gray90,
'tile-enabled-label-color': CColors.gray30,
'tile-enabled-title-color': CColors.gray10,
'tile-enabled-description-color': CColors.gray30,
//
'tile-disabled-background-color': CColors.gray90,
'tile-disabled-label-color': CColors.gray70,
'tile-disabled-title-color': CColors.gray70,
'tile-disabled-description-color': CColors.gray70,
};
}
tile.widget.dart
class CTile extends StatelessWidget {
const CTile({
Key? key,
this.enable = true,
...
}) : super(key: key);
final bool enable;
final _colors = CTileStyle.colors;
final _layouts = CTileStyle.layouts;
@override
Widget build(BuildContext context) {
/// styles helpers
String cwidget = 'tile';
String state = enable ? 'enabled' : 'disabled';
return IgnorePointer(
ignoring: !enable,
child: Container(
color: _colors['$cwidget-$state-background-color'],
padding: _layouts['$cwidget-padding'],
child: ....,
),
);
}
}
More examples can be found Here
Conclusion
In this article, we covered the 4 main things you need in large application development.
Here they are in summary:
- Construct your application as a set of features working together.
- Define the type of each dart file using file_name.type.dart.
- Using MVVM to manage your state is easier than other alternatives such as BLoC.
- Separate your widgets styles from the presentation code.
Source code
A full example for this article can be found here
Top comments (4)
This idea sounds very interesting. A scalable infrastructure is basically an environment that provides top-notch features to your users without compromising on speed and security. It doesn't matter how millions or billions of users your app is faciliating. It confers all of features without any downtime.
You should also read this post blog.back4app.com/mobile-app-infra... that tells about how you can build a scalable infrastrcuture and what's its definition. The main components that should be perfect and scalable in this regard are:
Thanks a lot @miachrist for this informative article, really appreciated 😍🙏
Nice. I like the idea. This will add a little more flavor to my already existing feature based folder structure.
Also, I like your PMVVM way and package. Looks intuitive and clean.
Thanks for the contribution, freaking out