└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Guidelines 2 | 3 | Hello! This document contains my personal recommendations for policies, procedures, and standards regarding how development should be conducted. 4 | 5 | ## Project code structure 6 | 7 | The project code should be structured with a mix of **folder-by-feature** and **folder-by-type**. 8 | 9 | Meaning at the root level you have folders defining features. For example: 10 | 11 | ``` 12 | |-- common/ 13 | |-- authentication/ 14 | |-- settings/ 15 | ``` 16 | 17 | Common/Core feature contains classes, functions or widgets that by default should be available in any features. 18 | 19 | So the idea is to have features of your application divided by top level folders. 20 | 21 | Think of every top level folders as a separate dart/flutter package, dependencies or access boundary of your 22 | application. 23 | 24 | So if you add something to the common folder, you have to ask yourself, will 99.% features of our application need that 25 | ? 26 | 27 | If you think of it as a dependency, the common dependency is always declared in your pubspec.yaml. 28 | 29 | So before adding a class, function or a widget to the common folder, ask yourself: 30 | 31 | * Is it something that every feature need in there pubspec.yaml, or it’s more something that’s optional ? 32 | * If the answer is YES than add it to common, if the answer is NO than extract it to another folder. 33 | 34 | Inside each feature folder you have folders defined by type. 35 | 36 | Here are the folder types that would be part of a feature folder. 37 | 38 | ``` 39 | |-- ui/ 40 | |-- cubits/ 41 | |-- domain_objects/ 42 | |-- exceptions/ 43 | |-- use_cases 44 | |-- services/ 45 | |-- repositories/ 46 | |-- data_sources/ 47 | |-- dtos/ 48 | ``` 49 | 50 | If there's more than one file related to a single file, you can group them in a folder, for example generated code for a 51 | class in a file. For example: 52 | 53 | ``` 54 | |-- authentication/ 55 | |---- cubits/ 56 | |------ login/ 57 | |-------- login_cubit.dart 58 | |-------- login_cubit.freezed.dart 59 | |-------- login_cubit.g.dart 60 | ``` 61 | 62 | ## User Interface (UI) 63 | 64 | Code that is related to the user's device interface, for example: UI PageBuilders, UI Screens, UI View, UI Components. 65 | 66 | ### Screen 67 | 68 | A screen is a user interface component that fill the whole device display and is the container of user interface view or 69 | component (Button, Checkboxes, Images and ect). 70 | 71 | Also, a screen is a specific application navigation destination. 72 | 73 | Guideline: 74 | 75 | - Screens filename and classes should be suffixed with **Screen**. 76 | - Screens classes should only interact with cubit classes. 77 | - Screen build method should be divided by private widgets that separate its **update area**/**use case**. For example: 78 | 79 | ```dart 80 | class LoginScreen extends StatelessWidget { 81 | //...fields and constructor... 82 | @override 83 | Widget build(BuildContext context) { 84 | return Column( 85 | children: [ 86 | _LoginLogo, 87 | _LoginForm, 88 | _LoginActions, 89 | _LoginFooter 90 | ], 91 | ); 92 | } 93 | } 94 | 95 | // extracted by use case. 96 | class _LoginLogo extends StatelessWidget {} 97 | 98 | // extracted by update area. 99 | class _LoginForm extends StatefullWidget {} 100 | 101 | // extracted by update area. 102 | class _LoginActions extends StatelesssWidget {} 103 | 104 | // extracted by update area. 105 | class _LoginFooter extends StatelessWidget {} 106 | ``` 107 | 108 | - After extracting to private widgets. If the screen file number of lines is greater than 400, 109 | you should move the private widgets to a part file. Part file naming convention for screen: 110 | ${name of the screen file}_components.dart. For example: 111 | 112 | `login_screen.dart` 113 | 114 | ```dart 115 | part 'login_screen_components.dart'; 116 | 117 | class LoginScreen extends StatelessWidget { 118 | //...fields and constructor... 119 | @override 120 | Widget build(BuildContext context) { 121 | return Column( 122 | children: [ 123 | _LoginLogo, 124 | _LoginForm, 125 | _LoginActions, 126 | _LoginFooter 127 | ], 128 | ); 129 | } 130 | } 131 | 132 | // Is kept here because it's does not break the 400 max line rule. 133 | class _LoginLogo extends StatelessWidget {} 134 | ``` 135 | 136 | `login_screen_components.dart` 137 | 138 | ```dart 139 | part of 'login_screen.dart'; 140 | 141 | /// [LoginScreen]'s fields. 142 | class _LoginForm extends StatelessWidget {} 143 | 144 | // ************************ Footer ************************ 145 | 146 | /// [LoginScreen]'s footer. 147 | class _LoginFooter extends StatelessWidget {} 148 | 149 | // ************************* ACTIONS ********************************* 150 | 151 | /// [LoginScreen]'s actions. 152 | class _LoginActions extends StatelessWidget {} 153 | ``` 154 | 155 | - Do not pass the cubit to screen constructors, instead access them using BlocBuilder, BlocListener or BlocConsumer. 156 | For example: 157 | 158 | ```dart 159 | class LoginScreen extends StatelessWidget { 160 | @override 161 | Widget build(BuildContext context) { 162 | return BlocBuilder( 163 | // Here cubit is not specified either. 164 | builder: (BuildContext context, LoginCubitState state) {}, 165 | ); 166 | } 167 | } 168 | ``` 169 | 170 | - Use private widget classes instead of functions to build subtrees, for example: 171 | 172 | ```dart 173 | class SomeWidget extends StatelessWidget { 174 | @override 175 | Widget build(BuildContext context) { 176 | //... 177 | } 178 | } 179 | 180 | class _SubTree1 extends StatelessWidget {} 181 | 182 | class _SubTree2 extends StatelessWidget {} 183 | 184 | class _SubTree3 extends StatelessWidget {} 185 | 186 | class _SubTree4 extends StatelessWidget {} 187 | ``` 188 | 189 | - A screen action/event callback should be part of the screen class as a method. 190 | * Action/Event callback method should start with **on** prefix. 191 | * Name of an action/event should match its use case. 192 | For example: 193 | 194 | ```dart 195 | class LoginScreen extends StatelessWidget { 196 | @override 197 | Widget build(BuildContext context) { 198 | return Column( 199 | childreen: [ 200 | Button1(onClick: _openRegisterUser), 201 | Button2(onClick: _openLogin), 202 | Field(onTextChanged: _onUserNameTextChanged), 203 | ], 204 | ); 205 | } 206 | 207 | void _openRegisterUser() {} 208 | 209 | void _openLogin() {} 210 | 211 | void _onUserNameTextChanged(String newText) {} 212 | } 213 | ``` 214 | 215 | ### UI View 216 | 217 | A view is a user interface component that does not fill the whole device display and is the container of user interface view or 218 | component (Button, Checkboxes, Images and ect). 219 | 220 | Also, a view is not an application navigation destination. 221 | 222 | Guideline: 223 | 224 | - View filename and classes should be suffixed with **View**. 225 | - View classes should only interact with cubit classes. 226 | - Screen build method should be divided by private widgets that separate its **update area**/**use case**. For example: 227 | 228 | ## Cubits 229 | 230 | [Cubits](https://pub.dev/packages/flutter_bloc) are classes that contains business logic for your UI, cubit is bound to 231 | a state, that represent the UI state. 232 | 233 | Guideline Recommendations: 234 | 235 | - Only cover business logic needed for the UI. 236 | - Return Futures for public actions. 237 | - Only interact with high level classes, meaning: Use cases. 238 | - Return final actions result to caller. For example: 239 | 240 | ```dart 241 | Future bookAppointment(BookingData booking); 242 | 243 | Future login(String username, String password); 244 | ``` 245 | 246 | - Only use **Repositories**, **Services** interfaces not concrete implementations. 247 | - Suffix your class with Cubit, example LoginCubit. 248 | - Cubit class files should be under the cubits folder of the feature. 249 | 250 | ### Cubit's State. 251 | 252 | A cubit state class represent the state of ui bounded to it a given time. 253 | 254 | Guideline Recommendations: 255 | 256 | - Use a single class for state or define a base class and define subclass for each state. 257 | - Final actions result should not be part of the state. for example: 258 | 259 | ```dart 260 | class LoginState { 261 | bool logginSuceeded // avoid this. 262 | } 263 | ``` 264 | 265 | - Keep the class in the same file were the Cubit class is defined. 266 | 267 | ## Use Cases 268 | 269 | Classes that used for making a single business operation/goal/job/task in your domain. 270 | 271 | Guidelines: 272 | - UseCase name should always have a **prefix**, which should be a verb, that describes the job that this class is doing. Ex: `class GetCurrentUserUseCase`, `class SignInUseCase`; 273 | - UseCase name should always have a **suffix** `UseCase`; 274 | - UseCase class should always have a public function `call()`, where return type is the result of executing the use case; 275 | - UseCase should have access only to `Repositories`, `Services` or any other `high level coordinators`; 276 | - UseCase **shouldn't** have access to `DataSource` or `Cubit` or interact with dtos or any other low level object; 277 | - UseCase should be used in `Cubit` or in other `UseCases`. 278 | 279 | Examples: 280 | ```dart 281 | class GetCurrentUserUseCase { 282 | final UserRepository _repository; 283 | const GetCurrentUserUseCase(this._repository); 284 | Future call() async { 285 | await _repository.getCurrentUser(); 286 | } 287 | } 288 | ``` 289 | 290 | ```dart 291 | class AuthenticateMemberUseCase { 292 | /// Create a [AuthenticateMemberUseCase]. 293 | const AuthenticateMemberUseCase( 294 | /* constructor arguments */ 295 | ); 296 | 297 | /* ... fields ...*/ 298 | 299 | /// Execute the use case. 300 | Future> call(MemberAuthenticationCredentials credentials) { 301 | return runTaskSafelyAsync(() async { 302 | final bool isEmailValid = credentials.email.isEmail; 303 | 304 | if (!isEmailValid) { 305 | throw const TegEmailInvalidException(); 306 | } 307 | 308 | final bool isConnected = await _hostDeviceInternetService.isConnected(); 309 | 310 | if (!isConnected) { 311 | throw const TegInternetUnavailableException(); 312 | } 313 | 314 | final String? deviceId = await _hostDeviceInfoRepository.getDeviceId(); 315 | 316 | if (deviceId == null) { 317 | throw const TegDeviceIdUnavailableException(); 318 | } 319 | 320 | final PublicPrivateKeyPair keyPair = await _keyGenerator.generate(); 321 | 322 | final Member member = await _authService.signIn( 323 | email: credentials.email, 324 | password: credentials.password, 325 | deviceId: deviceId, 326 | publicKey: keyPair.publicKey, 327 | ); 328 | 329 | await _updateCurrentMemberUseCase( 330 | member: member, 331 | memberPrivateKey: keyPair.privateKey, 332 | deviceId: deviceId, 333 | ); 334 | 335 | await _saveMemberAuthenticationCredentialsUseCase(credentials); 336 | 337 | final TegTask> memberAccountsTask = await _getMemberAccountsUseCase(); 338 | 339 | if (memberAccountsTask.failed) { 340 | throw memberAccountsTask.exception; 341 | } 342 | 343 | await _updateMemberCurrentAccountUseCase( 344 | member: member, 345 | deviceId: deviceId, 346 | memberPrivateKey: keyPair.privateKey, 347 | account: memberAccountsTask.result.first, 348 | ); 349 | }); 350 | } 351 | } 352 | ``` 353 | 354 | ## Repositories 355 | 356 | Classes that provide access to data using **only** **CRUD** operations for a specific feature or a scope of a feature, 357 | for example: 358 | 359 | ```dart 360 | abstract class BookingRepository { 361 | Futute getBookingById(String id); 362 | 363 | Future> getBookings(); 364 | 365 | Future deleteBookingById(String id); 366 | 367 | Future saveBooking(Booking booking); 368 | } 369 | ``` 370 | 371 | Guideline Recommendations: 372 | 373 | - Suffix class name with Repository. 374 | - Only define repositories for features or scope of features and name them so. 375 | - Use repositories to provide access to data and manipulate data. 376 | - Repositories should coordinating access between data sources or decide which data source to use for example by feature 377 | flag. Example: 378 | 379 | ```dart 380 | class DefaultBookingRepository implements BookingRepository { 381 | final LocalBookingDataSource local; 382 | final RemoteBookingDataSource remote; 383 | 384 | Future getBookingById(String id) async { 385 | Booking? savedBooking = await local.getBookingById(id); 386 | 387 | if (savedBooking == null) { 388 | Booking? remoteBooking = await remote.getBookingById(id); 389 | 390 | if (remoteBooking != null) { 391 | await local.saveBooking(remoteBooking); 392 | } 393 | return remoteBooking; 394 | } 395 | return savedBooking; 396 | } 397 | 398 | //...other operations... 399 | } 400 | ``` 401 | 402 | - Define interfaces for repository api and concrete implementation by type, for example: 403 | 404 | Interface 405 | 406 | ```dart 407 | abstract class BookingRepository {} 408 | ``` 409 | 410 | Implementations 411 | 412 | ```dart 413 | class DefaultBookingRepository implements BookingRepository {} 414 | 415 | class AnonymousBookingRepository implements BookingRepository {} 416 | 417 | class PremiumBookingRepository implements BookingRepository {} 418 | ``` 419 | 420 | - Do not use tools, platform related libraries nor perform network logic in repositories, encapsulate that in data 421 | sources. 422 | - Only use data sources inside repositories. 423 | - Only receive as input primitive or domain object and output domain objects. 424 | - Implementations of the **Repository API** should be named with the **repository class** name as suffix. 425 | - Avoid naming repositories with **Impl** suffix, name them using **Default** prefix + **Base repositories name**. 426 | - Return Asynchronous type for public API (Future or Stream). 427 | - Implementation of the repository should not change their public API (return type, method arguments). 428 | - Repository class files should be under the repositories folder of the feature. 429 | 430 | ## Services 431 | 432 | Classes that provide access to functionalities for a specific feature or a scope of a feature, for example: 433 | 434 | ```dart 435 | abstract class AuthenticationService { 436 | Future authenticate(String username, String password); 437 | 438 | //...other functionalities... 439 | } 440 | 441 | abstract class AppointmentService { 442 | Future register(Appointment appointment); 443 | 444 | //...other functionalities... 445 | } 446 | ``` 447 | 448 | Guideline Recommendations. 449 | 450 | - Suffix class name with **Service**. 451 | - Only define services for features or scope of features and name them so. 452 | - Use services to provide functionalities required to implement a business logic. 453 | - Use data sources to access to data required to implement a functionality business logic. 454 | - Do not use tools, platform related libraries nor perform network logic in services, implement it in data sources. 455 | - Only use data sources inside services. 456 | - Only receive as input primitive or domain object and output domain objects. 457 | - Implementations of the **Service API** should be named with the **service class** name as suffix. 458 | - Avoid naming services with **Impl** suffix, name them using **Default** prefix + **Base service name**. 459 | - Return Asynchronous type for public API (Future or Stream). 460 | - Implementation of the service should not change their public API (return type, method arguments). 461 | - Service class files should be under the services folder of the feature. 462 | 463 | ## Data sources 464 | 465 | Classes that implement access to data located locally or remotely. Example 466 | 467 | ```dart 468 | abstract class LocalBookingDataSource { 469 | Futture saveBooking(Booking booking); 470 | 471 | //...other functionalities... 472 | } 473 | 474 | abstract class RemoteBookingDataSource { 475 | Future saveBooking(); 476 | } 477 | ``` 478 | 479 | Guideline Recommendations: 480 | 481 | - Keep the base type of data sources to local and remote. 482 | - Name implementation after library or tool used and the data source name as suffix, for example: 483 | 484 | ```dart 485 | class SQLiteBookingDataSource implements LocalBookingDataSource { 486 | //...............Implementation................ 487 | } 488 | 489 | class RestApiBookingDataSource implements RemoteBookingDataSource { 490 | //...............Implementation................ 491 | } 492 | 493 | class MemoryBookingDataSource implements LocalBookingDataSource { 494 | //...............Implementation................ 495 | } 496 | ``` 497 | 498 | - Handle library or tool errors in data source and convert them to app custom exception. 499 | - Only receive as input primitive or domain object and output domain objects or primitive. 500 | - Implementations of the **Data Source API** should be named with the **Data Source class** name as suffix. 501 | - Return Asynchronous type for public API (Future or Stream). 502 | - Implementation of the data sources should not change their public API (return type, method arguments). 503 | - Data sources class files should be under the datasources folder of the feature. 504 | 505 | ## Data Transfer Objects - DTO 506 | 507 | Data transfer objects - used to transfer data from one system to another. Example 508 | 509 | ```dart 510 | class ApiRequest { 511 | //...fields... 512 | } 513 | 514 | class ApiResponse { 515 | //...fields... 516 | } 517 | ``` 518 | 519 | ```dart 520 | class SQLiteTableDefinition { 521 | //...fields... 522 | } 523 | ``` 524 | 525 | Guidelines: 526 | 527 | - Use DTO to represent database tables. 528 | - Use DTO to represent api request and response. 529 | - Use DTO to implement library required data. For example to use a persistence library your data need to extend an 530 | object or add more fields, create a DTO instead. 531 | - Implement mappers in the DTO class, for example. 532 | 533 | ```dart 534 | class ApiRequest { 535 | //...fields... 536 | 537 | factory ApiRequest.fromDO(DOObject doObject) { 538 | //...mapper code... 539 | } 540 | 541 | DOObject toDO() { 542 | //...mapper code... 543 | } 544 | } 545 | ``` 546 | 547 | - Do not implement saving or fetching logic in DTO, implement it in data sources. 548 | - DTO class files should be under the dtos folder of the feature. 549 | 550 | ## Domain Object 551 | 552 | Domain Object are data classes representation of a part of the business or an item within it, they are used to execute 553 | business logic independently of platform, library or tools. 554 | 555 | A domain object may represent, for example, **a person, place, event, business process, or concept** and **invoice, a 556 | product, a transaction or even details of a person**. 557 | 558 | Guideline: 559 | 560 | - Domain Object classes name should be suffixed with DO. 561 | - Domain Object classes should only contains data fields or method to format data fields. 562 | - Domain Object classes should implement equals and hash-code methods, fromJson and toJson. 563 | - Domain Object classes fields type should only be primitive or other domain object. 564 | - Domain Object class files should be under the domain_objects folder of the feature. 565 | 566 | ## How to structure assets 567 | 568 | The same as for Project code structure. 569 | 570 | First the feature name, then the `assets` folder containing files. 571 | 572 | Example: 573 | 574 | ``` 575 | |-- authentication/ 576 | |---- assets/ 577 | |------ password_hidden_icon.svg 578 | |------ forget_password_icon.svg 579 | |------ background.png 580 | ``` 581 | --------------------------------------------------------------------------------