# Source Code
Demi menjaga konsistensi sebuah projek, ikuti aturan dibawah ini.
# Penamaan File dan Folder
- nama file / folder ditulis dengan lower case dengan pemisah underscore (_).
- nama file / folder ditulis dengan jelas dan mempresentasikan objek yang sesuai.
- nama file / folder ditulis dalam bahasa inggris.
- nama file untuk icon ditulis dengan tambahan prefix
ic_
.- contoh :
ic_launcher.png
- contoh :
- nama file untuk image ditulis dengan tambahan prefix
img_
.- contoh :
img_banner.png
,
- contoh :
# Penamaan Class
- nama class ditulis camelCase dengan huruf awal ditulis CAPITAL.
- nama class ditulis dalam bahasa Inggris.
- contoh :
abstract class IProductRepository
.
- contoh :
- nama class khusus dalam
pages
ditulis dengan menambahkanPage
pada akhir.- contoh :
class ProductOverviewPage
,class ProductFormPage
,class ProductDetailPage
.
- contoh :
Penentuan page ditentukan jika didalam class tersebut terdapat widget Scaffold (opens new window)
# Penamaan Variable dan Function
- nama variable, function atau method ditulis dengan camelCase.
- nama variable ditulis dalam bahasa Inggris.
- nama variable atau function ditulis dengan jelas dan sesingkat mungkin. Sebisa mungkin hindari ambiguitas
- contoh :
String product
,final productItems
,void getProducts()
. - contoh salah :
String a
,String b
- contoh :
# Application Layer
Pada Application Layer berisi bloc yang merupakan penghubung antara layer Presentation dengan layer Infrastructure. Bloc mempunyai 3 file inti yang diperlukan, yaitu STATE, EVENT, dan BLOC. Penaamaan bloc ditulis dengan mengikuti aturan seperti berikut :
- gunakan Loader untuk proses GET yang sederhana, biasanya untuk kebutuhan menampilkan data.
- contoh :
product_loader_bloc.dart
- contoh :
- gunakan Form untuk proses yang membutuhkan data yang diproses / disimpan terlebih dahulu sebelum ditampilkan atau diproses. Biasanya diperlukan untuk input form.
- contoh :
product_form_bloc.dart
,login_form_bloc.dart
- contoh :
- gunakan Watcher untuk proses yang memerlukan Stream (data yang Real Time)
- contoh :
product_watcher_bloc.dart
- contoh :
- gunakan Actor untuk proses cepat pada perubahan UI. Biasanya untuk proses favorit item atau menghapus item.
- contoh :
product_actor_bloc.dart
- contoh :
# Structure Folder
├── application
│ └── product
│ │ ├── loader
│ │ │ ├── product_loader_bloc.dart
│ │ │ ├── product_loader_bloc.freezed.dart (generated file)
│ │ │ ├── product_loader_event.dart
│ │ │ └── product_loader_state.dart
│ │ ├── form
│ │ │ ├── product_form_bloc.dart
│ │ │ ├── product_form_bloc.freezed.dart (generated file)
│ │ │ ├── product_form_event.dart
│ │ │ └── product_form_state.dart
│ │ ├── actor
│ │ │ ├── product_actor_bloc.dart
│ │ │ ├── product_actor_bloc.freezed.dart (generated file)
│ │ │ ├── product_actor_event.dart
│ │ │ └── product_actor_state.dart
# Bloc State
# LoaderState atau WatcherState
@freezed
class ProductLoaderState with _$ProductLoaderState {
const factory ProductLoaderState.initial() = _Initial;
const factory ProductLoaderState.loadInProgress() = _LoadInProgess;
const factory ProductLoaderState.loadFailure(AppException failure) =
_LoadFailure;
const factory ProductLoaderState.loadSuccess(KtList<Product> products) =
_LoadSuccess;
}
Terkadang untuk menampilkan list data. Kita membutuhkan variabel temporary untuk menyimpan data tersebut. Biasanya diperlukan jika list data yang ingin kita tampilkan mempunyai sifat infinite Scroll List atau jika ingin menampilkan list data berdasarkan filter. Kita bisa mengubah class
LoaderState
dengan mengimplementasi sepertiFormState
.
@freezed
abstract class ProductLoaderState with _$ProductLoaderState {
const factory ProductLoaderState({
@Default(false) bool hasReachedMax,
required StringSingleLine queryName,
required KtList<Product> products,
required Option<AppException> failureOption,
}) = _ProductLoaderState;
factory ProductLoaderState.initial() => ProductLoaderState(
queryName: StringSingleLine(''),
products: const KtList.empty(),
failureOption: none(),
);
}
# ActorState
@freezed
class ProductActorState with _$ProductActorState {
const factory ProductActorState.initial() = _Initial;
const factory ProductActorState.actionInProgress() = _LoadInProgess;
const factory ProductActorState.deleteFailure(AppException failure) =
_LoadFailure;
const factory ProductActorState.deleteSuccess(KtList<Product> products) =
_LoadSuccess;
}
# FormState
@freezed
class ProductFormState with _$ProductFormState {
const factory ProductFormState({
required Product product,
required Option<Either<AppException, Unit>> failureOrSuccessOption,
@Default(false) bool isSubmitting,
@Default(false) bool isEditing,
@Default(false) bool showErrorMessages,
}) = _ProductFormState;
factory ProductFormState.initial() => ProductFormState(
product: Product.empty(),
failureOrSuccessOption: none(),
);
}
# Bloc Event
Penamaan event pada bloc silahkan ikuti aturan berikut:
- Penamaan functions ditulis dalam bahasa Inggris past tense.
- Penamaan harus jelas dan tidak ambigu.
@freezed
class ProductFormEvent with _$ProductFormEvent {
const factory ProductFormEvent.initialized(Option<Product> initialData) =
_Initialized;
const factory ProductFormEvent.nameChanged(String name) = _NameChanged;
const factory ProductFormEvent.descriptionChanged(String description) =
_DescriptionChanged;
const factory ProductFormEvent.priceChanged(num price) = _PriceChanged;
const factory ProductFormEvent.submitted() = _Submitted;
}
# Presentation Layer
# Structure Folder
├── pages
│ └── product
│ │ ├── overview
│ │ │ ├── widgets
│ │ │ │ └── card_product.dart
│ │ │ └── overview_page.dart
│ │ ├── form
│ │ │ ├── widgets
│ │ │ │ └── name_field.dart
│ │ │ └── form_page.dart
│ │ ├── detail
│ │ │ ├── widgets
│ │ │ │ └── header.dart
│ │ │ └── form_page.dart
Pisahkan widget UI menjadi bentuk komponen-komponen kecil yang nantinya ditaruh dalam folder
widgets
untuk memudahkan tim dalam proses maintenance.
# Domain layer
# Structure Folder
├── domain
│ ├── product
│ │ ├── product.dart
│ │ ├── product.freezed.dart (generated file)
│ │ ├── value_objects.dart
│ │ └── i_product_repository.dart
# Rule
- Setiap Domain memiliki Value Object-nya masing-masing. Value Object adalah boundary dari domain
/// mengharuskan value data untuk tidak boleh empty (''). dan tidak boleh multiLine (\n)
class ProductName extends ValueObject<String> {
@override
final Either<ValueFailure<String>, String> value;
factory ProductName(String input) {
return ProductName._(
validateStringNotEmpty(input).flatMap(validateStringStringLine));
}
const ProductName._(this.value);
}
/// mengharuskan value data untuk tidak boleh empty. dan maksimal input karakter 1000
class ProductDescription extends ValueObject<String> {
@override
final Either<ValueFailure<String>, String> value;
static const maxLength = 1000;
factory ProductDescription(String input) {
return ProductDescription._(validateMaxStringLength(input, maxLength).flatMap(validateStringStringLine));
}
const ProductDescription._(this.value);
}
class ProductPrice extends ValueObject<num> {
@override
final Either<ValueFailure<num>, num> value;
factory ProductPrice(num input) {
return ProductPrice._(validateNominalValue(input));
}
const ProductPrice._(this.value);
}
- Setiap Domain memiliki
interface repository
.
abstract class IProductRepository {
Future<Either<AppException, KtList<Product>>> loadProducts();
Future<Either<AppException, Product>> loadProduct(UniqueId id);
Future<Either<AppException, Unit>> addProduct(Product product);
Future<Either<AppException, Unit>> editProduct(Product product);
Future<Either<AppException, Unit>> removeProduct(UniqueId id);
}
- Atribut dari Domain mempunyai tipe data dari Value Object-nya.
- Domain WAJIB terdapat
empty
atauinitial
Domain. - Domain WAJIB terdapat getter
failureOption
jika Domain tersebut diperlukan untuk pengisian data input oleh User.failureOption
diperlukan untuk mengecek Domain valid atau tidak
@freezed
class Product with _$Product {
const Product._();
const factory Product({
required UniqueId id,
required ProductName name,
required ProductDescription description,
required ProductPrice price,
}) = _Product;
// default domain
factory Product.empty() => Product(
id: UniqueId(),
name: ProductName(''),
description: ProductDescription(''),
price: ProductPrice(0),
);
// menerima data domain jika valid (none) atau tidak (some)
Option<ValueFailure<dynamic>> get failureOption {
return name.failureOrUnit
.andThen(description.failureOrUnit)
.andThen(price.failureOrUnit)
.fold(
(f) => some(f),
(_) => none(),
);
}
}
# Infrastructure Layer
# Structure Folder
├── infrasctructure
│ ├── product
│ │ ├── data_sources
│ │ │ ├── local_data_provider.dart # ... jika data dari local
│ │ │ └── remote_data_provider.dart # ... jika data dari server
│ │ ├── product_dtos.dart
│ │ ├── product_dtos.freezed.dart (generated file)
│ │ ├── product_dtos.g.dart (generated file)
│ │ └── product_repository.dart
Isi dari folder data source disesuaikan dengan sumber data yang dipakai. Contoh penamaan class dari file
local_data_provider.dart
adalahclass ProductLocalDataProvider {}
.
# Data Transfer Object (DTO)
Merupakan model yang menampung data dari extenal.
part 'product_dtos.freezed.dart';
part 'product_dtos.g.dart';
@freezed
class ProductDto with _$ProductDto {
const ProductDto._();
const factory ProductDto({
@JsonKey(name: 'id') required String id,
@JsonKey(name: 'product_name') required String name,
@JsonKey(name: 'product_description') required String description,
@JsonKey(name: 'product_price') required double price,
}) = _ProductDto;
// WAJIB, untuk generate model ProductDto yang nantinya digunakan untuk parsing ke/dari JSON
factory ProductDto.fromJson(Map<String, dynamic> json) =>
_$ProductDtoFromJson(json);
// untuk generate model ProductDto dari domain Product.
factory ProductDto.fromDomain(Product product) => ProductDto(
id: product.id.getOrCrash(),
name: product.name.getOrCrash(),
description: product.description.getOrCrash(),
price: product.price.getOrCrash().toDouble(),
);
// untuk generate domain Product dari model ProductDto
Product toDomain() => Product(
id: UniqueId.fromUniqueString(id),
name: ProductName(name),
description: ProductDescription(description),
price: ProductPrice(price),
);
}
# Data Source
Penulisan class RemoteDataProvider
atau class LocalDataProvider
ditulis dengan menambahkan nama data yang ingin di fetch. Contoh : ProductRemoteDataProvider
. Proses dilakukan dengan menggunakan package data_channel.
@injectable
class ProductRemoteDataProvider {
final ApiClient _apiClient;
ProductRemoteDataProvider(this._apiClient);
Future<DC<AppException, List<ProductDto>>> fetchProducts() async {
try {
final response = await _apiClient.get(ApiPath.getProducts);
if (response.statusCode == 200) {
final items = response.data['items'] as List;
final dtos = items
.map((e) => ProductDto.fromJson(e as Map<String, dynamic>))
.toList();
return DC.data(dtos);
}
return DC.error(
AppException.serverException(errorMessage: response.statusMessage));
} on AppException catch (e) {
return DC.error(e);
}
}
}
# Repository
Repository disini HARUS mengimplementasi dari Interface-nya.
@Injectable(as: IProductRepository)
class ProductRepository implements IProductRepository {
final ProductRemoteDataProvider _remoteDataProvider;
ProductRepository(this._remoteDataProvider);
@override
Future<Either<AppException, KtList<Product>>> loadProducts() async {
try {
final response = await _remoteDataProvider.fetchProducts();
if (response.hasError) {
return left(response.error!);
}
final products =
response.data!.map((e) => e.toDomain()).toImmutableList();
return right(products);
} catch (e, s) {
log('loadProducts', name: 'ProductRepository', error: e, stackTrace: s);
return left(const AppException.unexpectedException());
}
}
...
}
# Font
Jika ingin menggunakan font kustom pada app, gunakan package google_font (opens new window). Apabila jenis font yang diinginkan tidak ada, bisa mengikuti cara dari package flutter_gen_runner (opens new window) untuk generated asset font file
# Clean Code
Perhatikan tab Problems pada Visual Studio Code IDE. Sebisa mungkin tidak ada Warning maupun Error
# Constant
Optimalkan pemakaian const
pada widget UI. Penggunakan const
pada widget dapat mempengaruhi proses render UI, dikarenakan widget tersebut tidak perlu dirender ulang. Info lengkapnya silahkan lihat disini (opens new window).
# Factory
Jika terdapat widget atau fungsi yang dipakai dibeberapa tempat. Gunakan metode faktorisasi untuk mengurangi duplikasi.
# Dokumentasi
Pembuatan dokumentasi dilakukan untuk memudahkan tim dalam proses maintenance. Gunakan //
untuk pembuatan comment dan gunakan ///
untuk pembuatan dokumentasi pada fungsi atau class.
Contoh lebih lengkapnya disini (opens new window)