-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add CONTRIBUTING.md and PROJECT_ARCHITECTURE.md #6
Open
Vanethos
wants to merge
3
commits into
FlutterPortugal:master
Choose a base branch
from
Vanethos:feature/contributing
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,74 @@ | ||||||
# Contributing | ||||||
|
||||||
Everyone is welcome to contribute to the VOST application, including adding new screens, fixing current issues or suggesting enhancements. | ||||||
|
||||||
For discussion about the project, please join our [Discord Server](https://discord.gg/4tX46B) and check the #vostpt channel. | ||||||
|
||||||
If you see any issue in the app, feel free to open a new issue, documenting it in accordance to [Opening an Issue](#opening-an-issue). | ||||||
|
||||||
|
||||||
|
||||||
- [Project Details](#project-details) | ||||||
- [Opening an Issue](#opening-an-issue) | ||||||
- [Creating A Pull Request](#creating-a-pull-request) | ||||||
- [Style Guide](#style-guide) | ||||||
|
||||||
## Project Details | ||||||
|
||||||
### Setup | ||||||
|
||||||
This app is built with Flutter and Dart 2, follwing Bloc's design Pattern. | ||||||
|
||||||
### Run & Go | ||||||
|
||||||
``` | ||||||
$ git clone https://github.com/FlutterPortugal/vost-app | ||||||
``` | ||||||
|
||||||
Make sure you have installed Flutter following [these instructions](https://flutter.io/get-started/install/). | ||||||
|
||||||
``` | ||||||
$ cd vost-app | ||||||
$ flutter doctor | ||||||
``` | ||||||
|
||||||
Fix anything `$ flutter doctor` asks. | ||||||
Make sure you have a device connected (Simulator or Emulator or a real device connected) and then run | ||||||
|
||||||
``` | ||||||
$ flutter run | ||||||
``` | ||||||
|
||||||
You can set up [Visual Studio Code or IntelliJ IDEA/Android Studio](https://flutter.io/get-started/editor/) to do this for you. | ||||||
|
||||||
# Opening an Issue | ||||||
## New Feature/Feature Request | ||||||
A new feature should have the `enhanchement` label, with a title describing the new feature (“Add new endpoints for VOST - gas stations”). | ||||||
|
||||||
If the feature is an UI improvement or a new screen, please provide a wireframe of the screen, which can be made with apps such as [MockFlow WireframePro - Editor](https://wireframepro.mockflow.com/editor.jsp?editor=on&bgcolor=white&perm=Create&ptitle=Warranty%20Example&store=yes&category=M270f5278938129256fdb714d2bdd36cf1550137767039&projectid=Ma33dc569ed488dd5d67216b46ee7e0d71554926933286&publicid=aea96dad4cfb471b95fff6b1b2326b23&redirect=yes#/page/ab44169772fb4b4ca6a5e81f02d80e33). | ||||||
|
||||||
The issue message should explain what the feature is going to change in the app and possible conflicts (“The new endpoints will change the list item in the Home Screen”). | ||||||
|
||||||
## Issue or Bug | ||||||
When filing a bug, please follow the [Github Issue Template Guideline](https://gist.github.com/auremoser/72803ba969d0e61ff070#file-issue_template-md) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
# Creating a Pull Request | ||||||
When creating a Pull Request, please make sure your code follows the [Style Guide](#style-guide). | ||||||
|
||||||
This project follows the [GitFlow](https://datasift.github.io/gitflow/IntroducingGitFlow.html) structure in the git tree, so when creating a new feature or fixing an issue, please create the appropriate branch from the `development` branch. | ||||||
|
||||||
The branching will use the branch-per-issue workflow: for each open issue, we'll create a corresponding git branch. | ||||||
|
||||||
For instance, issue #123 should have a corresponding feature/API-123-ShortTaskDescription branch, which MUST branch off the latest code in development. | ||||||
|
||||||
Additionally, each commit message must follow this set of rules: | ||||||
- Reference the issue that you are fixing/adding as a feature | ||||||
- Explain what the commit does (“Fixes issue #1), “Adds a button to the bottom bar for ‘Home’”) | ||||||
- Use the present tense (“Add feature” not “Added feature”) | ||||||
- Use the imperative mood (“Move cursor to…” not “Moves cursor to…”) | ||||||
|
||||||
# Style Guide | ||||||
When creating a pull-request to the VOST repo, you must: | ||||||
- Format the code with `dartfmt` | ||||||
- Comment any added Functions and Methods according to the [Official Dart Recommendations](https://www.dartlang.org/guides/language/effective-dart/documentation) for commenting code. | ||||||
- Follow our [Project Architecture](https://github.com/FlutterPortugal/vost-app/blob/master/PROJECT_ARCHITECTURE.md) guidelines. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
## Project Architecture | ||
|
||
The VOST Application is created with [Flutter](https://flutter.dev/) following the [BLoC](https://www.didierboelens.com/2018/08/reactive-programming---streams---bloc/) architecture combined with [Clean Architecture](http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) .The app is divided into 3 sections: *Data, Domain* and *UI*. Each layer is responsible only for the logic contained in it and doesn’t know how the preciding and following layers work. This ensures that if one of the layer has to change, eg.: the API changes the endpoints or an object changes part of its fields, minimum impact should be propagated to the remaining layers. | ||
|
||
* *Data* is responsible to retrieve information from the network and store/fetch data saved into the SharedPreferences | ||
* *Domain* is responsible for manipulating the data received by the data layer. Furthermore, this is where part of the Business Logic of the application is located. | ||
* *UI* is composed of two sub-layers: *BLoC*s and *Widgets*. Widgets are resposible for displaying UI to the user, and should not manipulate data directly. The data shown to the user should be stored and manipulated by a *BLoC*. In most cases, each widget has a BLoC, and if needed, they can access and manipulate other BLoCs. | ||
Dependency Injection is handled by the DependencyProvider, which can be accessed by the Widgets via `DependencyProvider.of(context)`. | ||
|
||
### Data Layer | ||
#### Network | ||
All *API* calls are managed by [Dio](https://pub.dartlang.org/packages/dio) which can easily manage REST API calls and add headers and interceptors for every request. The following structure is used: | ||
* *Endpoints*: we declare the *method* (POST, PUT, GET, DELETE), the *json string* and the *endpoint* that we are calling. | ||
* *Services*: here we *map* the *Response Object* to a *Data Object* that can be used in our project. | ||
* *Models*: *Data Models* that represent the json object sent from the server. They are dividided into *Response*and *Request* objects and *Serializers*, needed for the json serialization and deserialization. | ||
* *Interceptors:* the interceptors used for the *requests*, *responsonses*,*errors* and respective *logging* | ||
|
||
#### Endpoints | ||
This class’s sole purpose is to *declare* the REST API *endpoints* and its necessary options, such as the *body* as a JSON string and the necessary *headers*. | ||
An endpoint can be declared as follow: | ||
```dart | ||
const todoEndpoint = "todos"; | ||
const categoryQuery = "category"; | ||
|
||
Future<Response> getData(String id, String categoryId) { | ||
return dio.get(“$todoEndpoint/$id”, queryParameters: {categoryQuery: categoryId}); | ||
} | ||
``` | ||
|
||
Each Endpoints class has a *dependency* to the *Dio* object created in DependencyProvider. | ||
*Services* and to the *Parser* so that it can *serialize* and *deserialize* son objects. | ||
|
||
This class sole purpose is to *map* the *Response*given by the Endpoints class to a data model class and to *Serialize to json* the *Request* objects. Each function can be declared as following: | ||
|
||
```dart | ||
Observable<MockDataRemote | ||
> getData(String id, String categoryId) { | ||
return Observable.fromFuture(_api.getData(accessId, categoryId) | ||
.map((response) => | ||
_parser.parseJsonObject(response.data, MockDataRemote.serializer)); | ||
} | ||
``` | ||
|
||
|
||
#### Models | ||
A data model is built with the [built_value](https://github.com/google/built_value.dart) and [build_runner](https://pub.dartlang.org/packages/build_runner) to generate *immutable* data class objects with a *builder* method. | ||
|
||
```dart | ||
import ‘package:built_value/built_value.dart’; | ||
import ‘package:built_value/serializer.dart’; | ||
import ‘package:vost/data/remote/models/_base/parser.dart’; | ||
|
||
part ‘mock_data_remote.g.dart’; | ||
|
||
abstract class MockDataRemote implements Built<MockDataRemote, MockDataRemoteBuilder>, SerializedModel<MockDataRemote> { | ||
MockDataRemote._(); | ||
|
||
static Serializer<MockDataRemote> get serializer => _$mockDataRemoteSerializer; | ||
|
||
int get userId; | ||
int get id; | ||
String get title; | ||
bool get completed; | ||
|
||
factory MockDataRemote([updates(MockDataRemoteBuilder b)]) = _$MockDataRemote; | ||
} | ||
``` | ||
|
||
All classes have to follow the structure provided above. To help in the creation of this class, you can use the following [Live Template](https://medium.com/flutter-community/live-templates-or-how-to-spend-less-time-writing-boilerplate-code-on-flutter-with-intellij-7fb2f769f23) | ||
|
||
```dart | ||
import ‘package:built_value/built_value.dart’; | ||
import ‘package:built_value/serializer.dart’; | ||
import 'package:vost/data/remote/models/_base/parser.dart'; | ||
|
||
part ‘$FILE_NAME$.g.dart’; | ||
|
||
abstract class $CLASS_NAME$ implements Built<$CLASS_NAME$, $CLASS_NAME$Builder>, SerializedModel<$CLASS_NAME$> { | ||
$CLASS_NAME$._(); | ||
|
||
static Serializer<$CLASS_NAME$> get serializer => _$$$STATIC_CLASS_NAME$Serializer; | ||
|
||
$END$ | ||
|
||
factory $CLASS_NAME$([updates($CLASS_NAME$Builder b)]) = _$$$CLASS_NAME$; | ||
} | ||
``` | ||
|
||
Additionally, every class that needs to be *serialized* and *deserialized* needs to be added to the `serializers.dart` file in the `@SerializersFor()` function. | ||
|
||
#### Shared Preferences | ||
This package is responsible for *adding* and *retrieving* *data* directly from the *Shared Preferences* using the [shared_prefs](https://pub.dartlang.org/packages/shared_preferences) plugin. | ||
To add a new object to the shared preferences, the following must be added | ||
```dart | ||
String getPrivatePhoneKey() { | ||
return _sharedPreferences.getString(_privatePhoneKey); | ||
} | ||
|
||
Future<bool> savePrivatePhoneKey(String key) async { | ||
return await _sharedPreferences.setString(_privatePhoneKey, key); | ||
} | ||
const _privatePhoneKey = “private_phone_key” | ||
``` | ||
|
||
Always keep the *keys* at the *bottom of the file* so that we can easily check what are the objects we are storing in memory | ||
|
||
### UI | ||
#### Blocs | ||
|
||
Each new bloc must extends the *BaseBloc*. This base class gives the common functionality of all blocs, such as error handling, displaying error message, showing loading messages and handling the dispose of the streams, sinks and subjects. | ||
|
||
To *communicate* with a bloc, we must add a *Sink* to receive either *data* or, if there is no data to add to the bloc, an *Event*. This sink is derived from the *RxDart Subject* that we are using. Furthermore, we must *add each subscribed Subject* to the *disposable* so that it can be disposed when the bloc is disposed. | ||
|
||
To *receive data* we must use a *Stream* in the *Bloc* and a *StreamBuilder* to display data in the UI. Adding data to the stream can be done using the `.add` method of the *RxDart Subject*. | ||
|
||
An example of a bloc that handles input and output is as follows: | ||
|
||
``` dart | ||
class HomeBloc extends BaseBloc { | ||
MockManager _mockManager; | ||
|
||
/// Event to fetch new data | ||
var _fetchNewDataSubject = PublishSubject<Event>(); | ||
|
||
Sink<Event> get fetchNewDataSink => _fetchNewDataSubject.sink; | ||
|
||
/// Event to relay MockData information to the UI | ||
var _mockDataSubject = BehaviorSubject<MockData>(); | ||
|
||
Stream<MockData> get mockDataStream => _mockDataSubject.stream; | ||
|
||
HomeBloc(this._mockManager) { | ||
disposable.add(_fetchNewDataSubject.stream | ||
.flatMap((_) => _mockManager.getMockData()) | ||
.listen(_mockDataSubject.add, onError: (error) { | ||
print(error); | ||
handleOnError(genericErrorMessage); | ||
})); | ||
} | ||
} | ||
``` | ||
|
||
#### Screens | ||
|
||
If we want our screen to display data and error and loading messages, we must extend the *BasePage* and *BaseState* classes. These classes setup the *init* methods and wires up the *bloc*. To be able to use this class, we must have a bloc to be used in this page. | ||
|
||
In VOST we are using *routing*, so each new page must be declared in the `app.dart`file either in the *routes*, for routes that we are not passing new items, or in *onGenerateRoutes*. We use the *BuildContext* to get the desired bloc from the *DependencyProvider*: | ||
|
||
```dart | ||
class VostApp extends StatelessWidget { | ||
@override | ||
Widget build(BuildContext context) { | ||
return MaterialApp( | ||
//... | ||
routes: { | ||
routeHome: (context) => HomePage(title: 'Vost App', bloc: DependencyProvider.of(context).getHomeBloc(),), | ||
}, | ||
); | ||
} | ||
} | ||
``` | ||
|
||
Finally, all navigation methods used in the app must all be in the `navigation.dart` file. When adding a new page, add a function with a name that clarifies where we are navigating to: | ||
|
||
```dart | ||
Future<void> navigateToHomePage(BuildContext context, String userId) { | ||
return Navigator.of(context).pushReplacementNamed(routeHome, arguments: userId); | ||
} | ||
``` | ||
|
||
|
||
### Assets | ||
|
||
When adding new assets to the app, add a new constant to the appropriate file in the *presentation/assets* folder. | ||
|
||
For example, if we are adding a new image, we edit (or create) an `images.dart` file in the *presentation/assets* folder with the following line: | ||
|
||
```dart | ||
const assetImageIllustrationPhone = ‘assets/illustration_phone.png’; | ||
``` | ||
|
||
When adding the file, it must be added in the */assets* folder, and the following must be added to the `pubspec.yaml` file if not present: | ||
|
||
```yaml | ||
flutter: | ||
assets: | ||
- assets/ | ||
``` | ||
|
||
### Dependency Injection | ||
Each new dependency should be declared in the *DependencyProvider*. | ||
- If the dependency is *not* required by a widget in the app, we initialise it in the *init* method. If this dependency is required by a bloc, we must declare a *class member variable* so that the bloc can be initialised. | ||
- If the dependency *is required* by a widget in the app (for example a Bloc), we declare a *getter* for it using the following structure: | ||
|
||
```dart | ||
HomeBloc _homeBloc; | ||
|
||
HomeBloc getHomeBloc({bool forceCreation = false }) { | ||
if(_homeBloc == null || forceCreation) { | ||
_homeBloc = HomeBloc(_cowManager); | ||
} | ||
return _homeBloc; | ||
} | ||
``` | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.