Skip to content

Latest commit

 

History

History
492 lines (345 loc) · 12 KB

CONTRIBUTING.md

File metadata and controls

492 lines (345 loc) · 12 KB

Contribution Guide

  1. Requirements
  2. Prerequisites
  3. Operations
  4. Structure overview
  5. Code style
  6. Backend connectivity

Requirements

  • If you don't use Docker-wrapped commands, make sure that tools you're using have the same version as in Docker-wrapped commands.

Prerequisites

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

Operations

Take a look at Makefile for command usage details.

Local development

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

Building

To build/rebuild project use the following Makefile command:

make build platform=apk
make build platform=apk dockerized=yes  # Docker-wrapped

Linting

To perform a static analysis of Dart sources use the following Makefile command:

make lint
make lint dockerized=yes  # Docker-wrapped

Formatting

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

Testing

To run unit tests use the following Makefile command:

make test.unit
make test.unit dockerized=yes  # Docker-wrapped

Documentation

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.

Cleaning

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

Structure overview

Project uses GetX package for navigation, state management, l10n and much more. So, it's required to become familiar with GetX first.

Domain layer

- 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 (user interface) layer

- 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.

Routing

Application uses custom Router routing.

Application has nested Routers (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.

l10n (localization)

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:

  1. Adding language's dictionary as a new .ftl file to assets/l10n/ directory.
  2. 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));

Code style

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.

Documentation

DO document your code. Documentation must follow Effective Dart official recommendations with the following exception:

  • prefer omitting leading A, An or The article.

Imports inside /lib directory

DO use absolute or relative imports within /lib directory.

🚫 Wrong

import '../../../../ui/widget/animated_button.dart'; // Too deep.
import 'package:messenger/ui/widget/modal_popup.dart'; // `package:` import.

👍 Correct

import '../animated_button.dart';
import '/ui/widget/modal_popup.dart';
import 'home/page/widget/animated_button.dart';
import 'widget/animated_button.dart';

Classes, constructors, fields and methods ordering

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):

  1. Default constructor
  2. Named/other constructors
  3. Public fields
  4. Private fields
  5. Public getters/setters
  6. Private getters/setters
  7. Public methods
  8. Private methods

🚫 Wrong

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,
}

👍 Correct

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 {
    // ...
}

Explicit dependencies injection

DO pass all the dependencies of your class/service/etc needs via its constructor.

🚫 Wrong

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();
    }
}

👍 Correct

class AuthService {
    AuthService(this.graphQlProvider, this.storageProvider//, ...);

    GraphQlProvider graphQlProvider;
    StorageProvider storageProvider;
    // ...
}

Explicit types

Do specify types explicitly to increase code readability and strictness.

🚫 Wrong

var authService = Get.find();
var counter = 0.obs;
var user = User().obs;
queryUsers() { 
    // ...
    return Map<...>;
}

👍 Correct

AuthService authService = Get.find();
RxInt counter = RxInt(0);
Rx<User> user = Rx<User>(User());
Map<...> queryUsers() { 
    // ...
    return Map<...>;
}

Type safety via new types

PREFER using New Type Idiom to increase type safety and to have more granular decomposition.

🚫 Bad

class User {
    String id;
    String? username;
    String? email;
    String? bio;
}

👍 Good

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;
}

Backend connectivity

Local development

Development GraphQL API playground is available here.

In order to connect to the development backend GraphQL endpoint, you should either use the following --dart-defines:

--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-defines to make e2e, make build or make run commands by specifying the dart-env parameter (see Makefile for usage details).