33
Building scalable Flutter apps (Architecture, Styling, Conventions, State management)
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.
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.
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.
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).
- 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
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
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.
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
.
1. Build your ViewModel
.
class MyViewModel extends ViewModel {
int counter = 0;
// Optional
void init() {
// It's called after the ViewModel is constructed
}
// Optional
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 {
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);
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);
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
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:
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.
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
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;
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
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.
A full example for this article can be found here