Flutter project for the Demo architecture
- Flutter architecture demo
-
Install flutter_gen in your system and run:
fluttergen -c pubspec.yaml
-
Install git hooks (optional)
./scripts/install-git-hooks.sh
-
Run
make check
This command comes useful when you want to make sure your code is clean and ready to be code-reviewed. it does the following:
- runs flutter_gen
- formats your code
- runs
flutter analyze
- runs unit tests and recreates all golden test screenshot files
- runs dart_code_metrics checks
The app follows clean architecture principles and is divided into 4 modules:
The UI Module contains Flutter widgets, animations, routes, pages and all the code that is responsible for the appearance of the app. Furthermore, it is important to note that the module omits logic.
Home for Presenter
and PresentationModel
classes.
Presenters decide WHEN to display things. They trigger business logic from the domain
module as well as update
the PresentationModel
with relevant data, that is then used by the UI.
Moreover, the PresentationModel
stores data, and exposes it to the UI through the ViewModel
interface. This ensures
that the data is accessible to the UI in a read-only manner, and all internals relevant to presentation is not
visible.
UseCase
, Repository
and Entity
classes.
This module contains the business logic of the application. It decides WHAT should be done in the event of user
interaction. The logic is encapsulated into the UseCase
classes that communicate with the outer world through
repositories.
Repository interfaces are specified inside the domain
module and are implemented by the data
module. All the data
that domain
operates on is encapsulated into domain entity classes.
The Data module takes care of communicating with the “outer world” and interacts with third party libraries. Here we specify the REST API’s interactions, database access, SharedPrefs or external sensor access. All the data is then translated to domain entities that are agnostic of the libraries and technologies used in order to access them.
Navigation within the app uses the concept of mixin Routes and per-page Navigators.
Navigator is a class that consist of 0 or more Route
s mixins, its only job is to open another page. let's see it on an example:
Assume we have a navigator for the LoginPage
called LoginNavigator
:
class LoginNavigator with NoRoutes {
AppInitNavigator(this.appNavigator);
@override
final AppNavigator appNavigator;
}
by default, its mixed in with NoRoutes
mixin that is a placeholder to be replaced with other routes. This navigator is used
by LoginPresenter
to open other pages, lets add possibility to Navigate to DashboardPage
from within LoginNavigator
. We can do it
in the following way:
class LoginNavigator with DashboardRoute {
// ...
}
as you can see, we mixed in the DashboardRoute
Below you can see an implementation of the route
mixin DashboardRoute {
Future<void> openDashboard(DashboardInitialParams initialParams) async {
return appNavigator.push(
OnboardingPageTransition(
getIt<DashboardPage>(param1: initialParams),
),
);
}
AppNavigator get appNavigator;
}
it specifies the the openDashboard
method that now can be used on the LoginNavigator
in order to open DashboardPage. It uses
appNavigator
under the hood that is a wrapper around Flutter navigation that streamlines navigation, more on AppNavigator
below.
The takeaway from this is that Routes specify how given pages are opened, while Navigators gather those routes together and expose them to the caller by mixing them in.
AppNavigator
is a wrapper around Flutter navigation that allows us to perform navigation without the need of providing BuildContext
by default, it will use MaterialApp
's root navigator unless you provide BuildContext
explicitly in order to use nested navigation
through an embeded Navigator
widget from the flutter's SDK.
Below you can find a diagram showing the flow of control for a basic scenario of a login page:
Here is a list of different tools we use in the project that augment and help with our day-to-day jobs
In order to keep consistent version of flutter and dart for all developers in the team, we use fvm which is flutter version manager. we have a config commited to this repository under the .fvm/fvm_config.json file specifying exact version of flutter to use. After you install fvm on your machine, you can issue this command in the root directory to install proper flutter version:
fvm install
This repository host few useful file templates created with Mason. You can find
them in the templates
subdirectory. In order to use those,
install Mason (INSTRUCTIONS HERE)
🔴NOTE🔴 |
---|
You have to run following commands from the templates subdirectory! |
First, run mason get
to install all the templates locally!
command | description |
---|---|
mason get |
installs all templates on your local machine |
mason list |
shows the list of available templates |
mason make page |
executes the page template |
We host a set of our own custom lints inside the tools/custom_lints/
folder. those are written using the
custom_lint package. to run the lints, you have to run the following command:
fvm flutter pub run custom_lint
you can find the list of all the lints inside the custom_lint.dart
We use Fluttergen to properly generate index files for our assets, this way all the paths are saved into constants and you don't have to remembember about updating them or type them in manually whenever you need to use an asset.
after you install it in your system, you can run the following command:
fluttergen -c pubspec.yaml
in the root of project to generate Assets.gen.dart
files and others.
Dart_code_metrics is a library hosting bunch of additional custom lints.
We use the following commands to run it, (but please refer to Makefile for up-to-date list of commands)
fvm flutter pub run dart_code_metrics:metrics analyze lib --set-exit-on-violation-level=warning --fatal-style --fatal-performance --fatal-warnings
fvm flutter pub run dart_code_metrics:metrics check-unused-code . --fatal-unused
fvm flutter pub run dart_code_metrics:metrics check-unused-files . --fatal-unused --exclude="{templates/**,.dart_tool/**,lib/generated/**}"
we use fastlane for various tasks inside ios
and android
subfolders. Both platforms reuse a lot of code by referencing the
scripts inside tools/fastlane
folder. To understand more about what can be done with Fastlane, have a look at their respective READMEs:
Rule | Explanation |
---|---|
Add proper prefix to the PR title (feat: , fix: , chore: , refactor: ) |
Link |
Review your own PR first | Reading trough your own pr helps spot obvious errors and it saves time for the reviewer |
Make sure all CI checks pass | CI is meant to catch formatting and lint errors, make use of that and don't force others to do the machine's job :) |
Run the app and test it yourself | Before reviewing a PR or when issuing your own code, make sure to run the app and test it making sure the code doesn't break anything and works correctly |
Don't be afraid to ask questions | Code review is meant not only to find errors in someone's code, but it's also about making sure you understand the code and know what is going on. Don't be afraid to ask questions, if anything looks unclear to you, don't assume that it's your fault, ask a question! :) |
Rule | Explanation |
---|---|
Use trailing commas for method/constructor parameters and definitions | This way each param is in a separate line and adding new params is much easier to read in PRs |
Prefer named parameters | Whenever using more than one param, consider using named parameters, i.e: Bad:getBalances(true,"1283184") , Bad:getBalances(id: "1283184", refresh: true) |
- performs business logic, communicates with outer world through repositories
- should contain single public
execute()
method optionally with runtime arguments that performs business logic - all compile-time dependencies should be injected trough constructor
- never use json classes in usecases, always relay communication with APIs trough repositories
- should be specified as an interface in the
domain/repositories
package, i.e:UserRepository
- implementation should be placed in the
data/{tech}
package, i.e:RestApiUserRepository
in thedata/rest/
package - its job is abstract the communication with outer world (APIs, Blockchains, databases, shared prefs).
- translates the domain models into data models and vice versa. so that use cases can depend solely on the domain entities
- repository methods should always return the response in the form of
Future<Either<Failure,Result>>
type - the
Result
part needs to be a domain entity or a primitive type, never a json or library-specific class!- Good:
String
,User
,int
- Bad:
UserJson
,FirebaseUser
,FirebaseAuthResponse
- Good:
- the
Failure
should be a class specified in domain entities subfolder (see Failures)
- should be stored in
domain/model/failures
- a specific domain entity that denotes a failure of certain action. Failure should contain a:
type
cause
String toString()
methodDisplayableFailure displayableFailure()
method (useful to be able to display a error dialog stating the cause of a failure)
enum VerifyPasscodeFailureType {
Unknown,
ValidationError,
}
class SomeFailure {
// ignore: avoid_field_initializers_in_const_classes
const SomeFailure.unknown([this.cause]) : type = SomeFailureType.Unknown;
const SomeFailure.validationError(PasscodeValidationFailure fail)
: cause = fail,
type = SomeFailureType.ValidationError;
final SomeFailureType type;
final dynamic cause;
DisplayableFailure displayableFailure() {
switch (type) {
case SomeFailureType.Unknown:
return DisplayableFailure(
title: strings.failureTitle,
message: strings.failureMessage,
);
}
}
@override
String toString() {
return 'SomeFailure{type: $type, cause: $cause}';
}
}
- should be stored in
domain/model
- domain entities should extend from
Equatable
- all fields should be final (so that we encourage immutability)
- all fields should be non-nullable unless the null value makes sense in the given context
class User extends Equatable {
final String username;
const User({
required this.username,
required this.firstName,
required this.lastName,
});
@override
List<Object> get props =>
[
username,
firstName,
lastName,
];
}
- reacts to user interaction, (i.e: all onTap methods are being forwarded to presenter)
- calls use cases
- updates presentation model with new data
- uses cubit to emit state (
presentationModel
) - never accesses initialParams from PresentationModel directly, but through the getters in presentationModel
- does not store any data, all the data that it requires is being held inside the presentationModel
- presenter never needs a
BuildContext
, it's always agnostic of the fact it's used in flutter thus should never rely on flutter classes
- interface that exposes data from presentation model to pages
- contains only getters, views are not supposed to mutate any state directly
- should not expose data that is internal for presenters and should not be used by pages directly
- stores data used by presenter and pages
- accepts InitialParams as constructor param
- never accesses initialParams directly
- routes all the user interaction to presenter, i.e:
InkWell(onTap: () => presenter.onTapLogin()
- uses
BlocBuilder
widget to listen to changes inViewModel
- accesses data to display from
ViewModel
only - divides the UI into smaller widgets, extracted to separate files
- all fields in
*Json
classes should be nullable - all fields should be final
- should contain
toDomain()
method that translates the json to domain entity
class UserJson {
UserJson({
required this.username,
required this.firstName,
required this.lastName,
});
factory UserJson.fromJson(Map<String, dynamic> json) =>
UserJson(
username: json['username'] as String? ?? '',
firstName: json['first_name'] as String? ?? '',
lastName: json['last_name'] as String? ?? '',
);
final String? username;
final String? firstName;
final String? lastName;
Balance toBalanceDomain() =>
Balance(
username: username ?? "",
firstName: firstName ?? "",
lastName: lastName ?? "",
);
}
Here are few rules/thoughts to remember when working on this project
Before adding new libraries to the project, please consult it with the team and tech leaders.
It's important to not pollute the code with low-quality libraries as it might impose code vulnerabilities
and make the maintenance much harder. If a solution provided by library is trivial, its better to host the code
inside the project rather than relying on third parties
// TODO