- If you don't use Docker-wrapped commands, make sure that tools you're using have the same version as in Docker-wrapped commands.
See "Get started" Flutter guide to set up Flutter development toolchain.
Use doctor
utility to run Flutter self-diagnosis and show information about the installed tooling:
flutter doctor # simple output
flutter doctor -v # verbose output
Take a look at Makefile
for command usage details.
To run the application use the following Makefile
command:
make run # in debug mode on a default device
make run debug=no # in release mode on a default device
make run device=chrome # in debug mode on Chrome (web) target
To build/rebuild project use the following Makefile
command:
make build platform=apk
make build platform=apk dockerized=yes # Docker-wrapped
To perform a static analysis of Dart sources use the following Makefile
command:
make lint
make lint dockerized=yes # Docker-wrapped
To auto-format Dart sources use the following Makefile
command:
make fmt
make fmt check=yes # report error instead of making changes in-place
make fmt dockerized=yes # Docker-wrapped
To run unit tests use the following Makefile
command:
make test.unit
make test.unit dockerized=yes # Docker-wrapped
To generate project documentation use the following Makefile
command:
make docs
make docs open=yes # open using `dhttpd`
make docs dockerized=yes # Docker-wrapped
In order for open=yes
to work you need to activate dhttpd
before running the command:
flutter pub activate global dhttpd
Note: Changing the deployment environment from dockerized=yes
to local machine and vice versa requires re-running make deps
since dartdoc
doesn't do it on its own.
To reset project and clean up it from temporary and generated files use the following Makefile
command:
make clean
make clean dockerized=yes # Docker-wrapped
Project uses GetX
package for navigation, state management, l10n and much more. So, it's required to become familiar with GetX
first.
- domain
- model
- ...
- user.dart
- repository
- ...
- user.dart
- service
- ...
- auth.dart
- provider
- ...
- graphql.dart
- store
- ...
- user.dart
Providers are stateful classes that work directly with the external resources and fetch/push some raw data.
Repositories are classes that mediate the communication between our controllers/services and our data. domain/repository/
directory contains interfaces for our repositories and their implementations are located in the store/
directory.
Services are stateful classes that implement functionality that is not bound to concrete UI component or domain entity and can be used by UI controllers or other services.
- ui
- page
- ...
- controller.dart
- view.dart
- widget
- ...
UI is separated to pages. Each page has its own UI component consisting of a View
and its Controller
. It may also contain a set of other UI components that are specific to this page only.
Controller
is a GetxController
sub-class containing a state of the specific View
. It may have access to the domain layer via repositories or services.
View
is a StatelessWidget
rendering the Controller
's state and with access to its methods.
Application uses custom Router
routing.
Application has nested Router
s (e.g. one in the GetMaterialApp
and one nested on the Home
page). So, a newly added page should be placed correspondingly in the proper Router
.
Application uses Fluent localization that is placed under assets/l10n/
directory. Any newly added .ftl
file should be named with a valid Unicode BCP47 Locale Identifier (i.e. en-US
, ru-RU
, etc).
Adding a new language means:
- Adding language's dictionary as a new
.ftl
file toassets/l10n/
directory. - Adding language to the
languages
mapping.
Using localization is as easy as adding .l10n
or .l10nfmt(...)
to the string literal:
Text('Hello, world'.l10n);
Text('Hello, world'.l10nfmt(arguments));
All Dart source code must follow Effective Dart official recommendations, and the project sources must be formatted with dartfmt.
Any rules described here are in priority if they have conflicts with Effective Dart recommendations.
DO document your code. Documentation must follow Effective Dart official recommendations with the following exception:
- prefer omitting leading
A
,An
orThe
article.
DO use absolute or relative imports within /lib
directory.
import '../../../../ui/widget/animated_button.dart'; // Too deep.
import 'package:messenger/ui/widget/modal_popup.dart'; // `package:` import.
import '../animated_button.dart';
import '/ui/widget/modal_popup.dart';
import 'home/page/widget/animated_button.dart';
import 'widget/animated_button.dart';
DO place constructors first in class, as stated in Flutter style guidelines:
This helps readers determine whether the class has a default implied constructor or not at a glance. If it was possible for a constructor to be anywhere in the class, then the reader would have to examine every line of the class to determine whether or not there was an implicit constructor or not.
The methods, fields, getters, etc should sustain a consistent ordering to help read and understand code fluently. First rule is public first: when reading code someone else wrote, you usually interested in API you're working with: public classes, fields, methods, etc. Private counterparts are consider implementation-specific and should be moved lower in a file. Second rule is a recommendation towards ordering of constructors, methods, fields, etc, inside a class. The following order is suggested (notice the public/private rule being applied as well):
- Default constructor
- Named/other constructors
- Public fields
- Private fields
- Public getters/setters
- Private getters/setters
- Public methods
- Private methods
class _ChatWatcher {
// ...
}
class Chat {
final ChatId id;
final ChatKind kind;
final Map<UserId, _ChatWatcher> _reads = {};
Chat.monolog(this.id) : kind = ChatKind.monolog;
Chat.dialog(this.id) : kind = ChatKind.dialog;
Chat.group(this.id) : kind = ChatKind.group;
Chat(this.id, this.kind);
void _ensureWatcher(UserId userId) {
// ...
}
void dispose() {
// ...
}
bool isReadBy(UserId userId) {
// ...
}
bool get isMonolog => kind == ChatKind.monolog;
bool get isDialog => kind == ChatKind.dialog;
bool get isGroup => kind == ChatKind.group;
}
class ChatId {
// ...
}
enum ChatKind {
monolog,
dialog,
group,
}
enum ChatKind {
monolog,
dialog,
group,
}
class Chat {
Chat(this.id, this.kind);
Chat.monolog(this.id) : kind = ChatKind.monolog;
Chat.dialog(this.id) : kind = ChatKind.dialog;
Chat.group(this.id) : kind = ChatKind.group;
final ChatId id;
final ChatKind kind;
final Map<UserId, _ChatWatcher> _reads = {};
bool get isMonolog => kind == ChatKind.monolog;
bool get isDialog => kind == ChatKind.dialog;
bool get isGroup => kind == ChatKind.group;
void dispose() {
// ...
}
bool isReadBy(UserId userId) {
// ...
}
void _ensureWatcher(UserId userId) {
// ...
}
}
class ChatId {
// ...
}
class _ChatWatcher {
// ...
}
DO pass all the dependencies of your class/service/etc needs via its constructor.
class AuthService {
AuthService();
GraphQlProvider? graphQlProvider;
StorageProvider? storageProvider;
//...
void onInit() {
super.onInit();
// or in any other place that is not CTOR
graphQlProvider = Get.find();
storageProvider = Get.find();
}
}
class AuthService {
AuthService(this.graphQlProvider, this.storageProvider//, ...);
GraphQlProvider graphQlProvider;
StorageProvider storageProvider;
// ...
}
Do specify types explicitly to increase code readability and strictness.
var authService = Get.find();
var counter = 0.obs;
var user = User().obs;
queryUsers() {
// ...
return Map<...>;
}
AuthService authService = Get.find();
RxInt counter = RxInt(0);
Rx<User> user = Rx<User>(User());
Map<...> queryUsers() {
// ...
return Map<...>;
}
PREFER using New Type Idiom to increase type safety and to have more granular decomposition.
class User {
String id;
String? username;
String? email;
String? bio;
}
class User {
UserId id;
UserName? username;
UserEmail? email;
UserBio? bio;
}
class UserId {
UserId(this.value);
final String value;
}
class UserName {
UserName(this.value);
final String value;
}
class UserEmail {
UserEmail(this.value);
final String value;
}
class UserBio {
UserBio(this.value);
final String value;
}
Development GraphQL API playground is available here.
In order to connect to the development backend GraphQL endpoint, you should either use the following --dart-define
s:
--dart-define=SOCAPP_HTTP_URL=https://messenger.soc.stg.t11913.org
--dart-define=SOCAPP_WS_URL=wss://messenger.soc.stg.t11913.org
--dart-define=SOCAPP_HTTP_PORT=443
--dart-define=SOCAPP_WS_PORT=443
--dart-define=SOCAPP_CONF_REMOTE=false
Or pass the following configuration to assets/conf.toml
:
[conf]
remote = false
[server.http]
url = "https://messenger.soc.stg.t11913.org"
port = 443
[server.ws]
url = "wss://messenger.soc.stg.t11913.org"
port = 443
Note, that you may pass --dart-define
s to make e2e
, make build
or make run
commands by specifying the dart-env
parameter (see Makefile
for usage details).