# 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
  • nama file untuk image ditulis dengan tambahan prefix img_.
    • contoh : img_banner.png,

# Penamaan Class

  • nama class ditulis camelCase dengan huruf awal ditulis CAPITAL.
  • nama class ditulis dalam bahasa Inggris.
    • contoh : abstract class IProductRepository.
  • nama class khusus dalam pages ditulis dengan menambahkan Page pada akhir.
    • contoh : class ProductOverviewPage, class ProductFormPage, class ProductDetailPage.

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

# 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
  • 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
  • gunakan Watcher untuk proses yang memerlukan Stream (data yang Real Time)
    • contoh : product_watcher_bloc.dart
  • gunakan Actor untuk proses cepat pada perubahan UI. Biasanya untuk proses favorit item atau menghapus item.
    • contoh : product_actor_bloc.dart

# 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 seperti FormState.

@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 atau initial 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 adalah class 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

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)