flutter_boilerplate_generator
Code generator untuk Flutter yang mengotomasi boilerplate pada Model dan CubitState: JSON serialization, entity/Hive mapping, copyWith, value equality, dan pattern-matching extensions.
Digunakan bersama flutter_boilerplate_annotations.
Daftar Isi
Instalasi
Tambahkan ke pubspec.yaml project Anda:
dependencies:
flutter_boilerplate_annotations: ^1.0.1
dev_dependencies:
build_runner: ^2.15.0
flutter_boilerplate_generator: ^2.0.0
Kemudian jalankan:
dart pub get
Setup
1. build.yaml (opsional, untuk kustomisasi output path)
Tanpa konfigurasi tambahan, file .g.dart akan di-generate di direktori yang sama dengan source file. Jika ingin memisahkan file generated ke subfolder generated/:
# build.yaml (di root project Anda)
targets:
$default:
builders:
flutter_boilerplate_generator|model:
generate_for:
- lib/**.dart
flutter_boilerplate_generator|state:
generate_for:
- lib/**.dart
source_gen:combining_builder:
options:
build_extensions:
"^lib/model/{{file}}.dart": "lib/model/generated/{{file}}.g.dart"
"^lib/state/{{file}}.dart": "lib/state/generated/{{file}}.g.dart"
2. Jalankan generator
dart run build_runner build
Untuk mode watch (otomatis regenerate saat file berubah):
dart run build_runner watch
Annotations
Import annotations dari package annotations:
import 'package:flutter_boilerplate_annotations/flutter_boilerplate_annotations.dart';
@Model
Annotation utama untuk generate boilerplate pada class model.
const Model({
Type? entity, // Class domain entity untuk mapping
Type? hiveObject, // Class Hive object untuk mapping
bool fromJson = true, // Generate factory fromJson
bool toJson = true, // Generate method toJson
bool fromEntity = false, // Generate factory fromEntity
bool toEntity = true, // Generate method toEntity
bool fromHiveObject = true, // Generate factory fromHiveObject
bool toHiveObject = true, // Generate method toHiveObject
bool genericAugmented = false, // Aktifkan dukungan generic type <T>
})
Persyaratan class:
- Harus
abstract - Harus menggunakan
with _$ClassName - Constructor utama harus
const factorydengan redirecting ke_ClassName - Deklarasi
partuntuk file generated
import 'package:flutter_boilerplate_annotations/flutter_boilerplate_annotations.dart';
part 'generated/product_model.g.dart';
@Model(entity: ProductEntity, fromEntity: true, toEntity: true)
abstract class ProductModel with _$ProductModel {
const factory ProductModel({
int? id,
String? name,
}) = _ProductModel;
factory ProductModel.fromJson(Map<String, dynamic> json) =>
_$ProductModelFromJson(json);
factory ProductModel.fromEntity(ProductEntity? entity) =>
_$ProductModelFromEntity(entity);
}
@CubitState
Annotation untuk generate sealed state class bergaya union type, lengkap dengan pattern-matching extensions.
const CubitState({
bool when = true, // Generate .when(...)
bool maybeWhen = true, // Generate .maybeWhen(...)
bool whenOrNull = true, // Generate .whenOrNull(...)
bool map = true, // Generate .map(...)
bool maybeMap = true, // Generate .maybeMap(...)
bool mapOrNull = true, // Generate .mapOrNull(...)
})
Persyaratan class:
- Harus
abstract - Setiap state adalah
const factoryyang meredirect ke class child - Nama child class diambil dari nama setelah
=
import 'package:flutter_boilerplate_annotations/flutter_boilerplate_annotations.dart';
part 'generated/product_state.g.dart';
@CubitState()
abstract class ProductState {
const factory ProductState.initial() = ProductInitial;
const factory ProductState.loading() = ProductLoading;
const factory ProductState.success({
required List<ProductModel> products,
@Default(0) int total,
}) = ProductSuccess;
const factory ProductState.failure({
required String message,
int? code,
}) = ProductFailure;
}
@JsonKey
Override nama key JSON untuk field tertentu.
class JsonKey {
final String? name; // Nama key di JSON
const JsonKey({this.name});
}
Penggunaan:
const factory ProductModel({
@JsonKey(name: 'product_name') String? name,
@JsonKey(name: 'created_at') DateTime? createdAt,
}) = _ProductModel;
JSON yang dihasilkan akan menggunakan product_name dan created_at sebagai key, bukan name dan createdAt.
@Default
Memberikan nilai default pada field. Digunakan di dalam parameter constructor.
class Default {
final Object? value;
const Default(this.value);
}
Penggunaan di Model:
const factory ProductModel({
@Default(0.0) double? price,
@Default(0) int? stock,
@Default(true) bool? isAvailable,
@Default([]) List<String>? tags,
}) = _ProductModel;
Penggunaan di CubitState:
const factory ProductState.success({
required List<ProductModel> products,
@Default(0) int total,
}) = ProductSuccess;
@EnumValue
Override nilai string yang digunakan saat enum di-serialize ke/dari JSON atau Hive.
class EnumValue {
final String value;
const EnumValue(this.value);
}
Penggunaan:
import 'package:flutter_boilerplate_annotations/flutter_boilerplate_annotations.dart';
enum ProductStatus {
@EnumValue('active')
active,
@EnumValue('inactive')
inactive,
@EnumValue('pending_review')
pendingReview, // → JSON value: "pending_review"
}
Tanpa @EnumValue, generator menggunakan nama field enum itu sendiri (pendingReview). Dengan @EnumValue, serialization menggunakan value yang ditentukan (pending_review).
Fitur yang Di-generate
Model
Semua fitur berikut di-generate otomatis dari satu @Model annotation:
== operator & hashCode
Membandingkan dua instance berdasarkan nilai seluruh field (value equality):
final a = ProductModel(id: 1, name: 'Apple');
final b = ProductModel(id: 1, name: 'Apple');
print(a == b); // true
print(a.hashCode == b.hashCode); // true
toString
Menghasilkan representasi string yang mudah dibaca:
print(product.toString());
// ProductModel(id: 1, name: Apple, price: 10000.0, ...)
copyWith
Membuat salinan instance dengan mengganti nilai field tertentu:
final updated = product.copyWith(
name: 'New Name',
price: 15000.0,
);
// Field yang tidak diisi tetap sama dengan original
Untuk field bertipe model (non-primitive), tersedia nested copyWith:
final updated = product.copyWith.address(
city: 'Jakarta',
);
toJson & fromJson
Serialisasi ke/dari Map<String, dynamic>:
// Serialize
final json = product.toJson();
// {'product_name': 'Apple', 'price': 10000.0, ...}
// Deserialize
final product = ProductModel.fromJson(json);
Tipe yang didukung:
| Dart Type | JSON Type |
|---|---|
int |
number |
double |
number |
String |
string |
bool |
boolean |
DateTime |
string (ISO 8601) |
Duration |
number (microseconds) |
List<T> |
array |
Map<K, V> |
object |
Enum |
string (via @EnumValue) |
| Nested model | object (via .fromJson()) |
Catatan: Field nullable yang bernilai
nullakan dihilangkan dari JSON output. SaatfromJson, field yang tidak ada di JSON akan bernilainull.
toEntity & fromEntity
Konversi antara model dan domain entity:
// Entity → Model
final model = ProductModel.fromEntity(productEntity);
// Model → Entity
final entity = model.toEntity();
Field yang namanya sama di antara model dan entity akan di-mapping otomatis. Field yang hanya ada di salah satu sisi diabaikan.
Untuk field bertipe model (nested), generator otomatis memanggil .fromEntity() / .toEntity() secara rekursif:
// ProductModel.address: AddressModel?
// ProductEntity.address: AddressEntity?
// → otomatis: AddressModel.fromEntity(entity.address)
// : addressModel.toEntity()
toHiveObject & fromHiveObject
Konversi antara model dan Hive object:
// Hive Object → Model
final model = ProductModel.fromHiveObject(productObject);
// Model → Hive Object
final hiveObject = model.toHiveObject();
Untuk field enum, generator otomatis menyimpan sebagai String (via .name) dan me-restore kembali ke enum saat membaca dari Hive. Tidak membutuhkan Hive adapter untuk enum.
Untuk nested model, generator otomatis memanggil .toHiveObject() / .fromHiveObject() secara rekursif.
CubitState
Child Classes
Generator membuat class concrete untuk setiap state:
// Dari:
const factory ProductState.success({required List<ProductModel> products}) = ProductSuccess;
// Di-generate:
class ProductSuccess with _$ComparablePropsMixin implements ProductState {
final List<ProductModel> products;
const ProductSuccess({required this.products});
@override List<Object?> get props => [products];
@override String toString() => ...;
}
Setiap child class mendapat value equality dan toString otomatis.
Pattern Matching Extensions
Generator menghasilkan 6 extension method untuk pattern matching:
when — semua case wajib dihandle:
final message = state.when(
initial: () => 'Siap',
loading: () => 'Loading...',
success: (products, total) => 'Ditemukan $total produk',
failure: (message, code) => 'Error: $message',
);
maybeWhen — hanya handle case tertentu, sisanya ke orElse:
final widget = state.maybeWhen(
success: (products, total) => ProductList(products: products),
orElse: () => const LoadingWidget(),
);
whenOrNull — handle case tertentu, sisanya return null:
final products = state.whenOrNull(
success: (products, total) => products,
);
map — menerima child instance, bukan field:
state.map(
initial: (s) => ..., // s is ProductInitial
loading: (s) => ..., // s is ProductLoading
success: (s) => s.products, // s is ProductSuccess
failure: (s) => s.message, // s is ProductFailure
);
maybeMap — sama seperti map tapi dengan orElse:
state.maybeMap(
success: (s) => ProductList(products: s.products),
orElse: () => const SizedBox(),
);
mapOrNull — sama seperti map tapi return null untuk case yang tidak dihandle:
final successState = state.mapOrNull(
success: (s) => s,
);
Contoh Lengkap
1. Enum dengan Custom Value
// lib/enum/product_status.dart
import 'package:flutter_boilerplate_annotations/flutter_boilerplate_annotations.dart';
enum ProductStatus {
@EnumValue('active') active,
@EnumValue('inactive') inactive,
@EnumValue('pending_review') pendingReview,
}
2. Domain Entity
// lib/entity/product_entity.dart
class ProductEntity {
final int? id;
final String? name;
final double? price;
final bool? isAvailable;
final DateTime? createdAt;
final List<String>? tags;
final ProductStatus? status;
final AddressEntity? address;
const ProductEntity({
this.id, this.name, this.price, this.isAvailable,
this.createdAt, this.tags, this.status, this.address,
});
}
3. Hive Object
// lib/hive/product_object.dart
class ProductObject {
final int? id;
final String? name;
final double? price;
final bool? isAvailable;
final String? status; // enum disimpan sebagai String
final AddressObject? address; // nested hive object
ProductObject({
this.id, this.name, this.price, this.isAvailable,
this.status, this.address,
});
}
4. Model dengan Semua Fitur
// lib/model/product_model.dart
import 'package:flutter_boilerplate_annotations/flutter_boilerplate_annotations.dart';
part 'generated/product_model.g.dart';
@Model(
entity: ProductEntity,
fromEntity: true,
toEntity: true,
hiveObject: ProductObject,
fromHiveObject: true,
toHiveObject: true,
)
abstract class ProductModel with _$ProductModel {
const factory ProductModel({
int? id,
@JsonKey(name: 'product_name') String? name,
String? description,
@Default(0.0) double? price,
@Default(0) int? stock,
@Default(true) bool? isAvailable,
DateTime? createdAt,
List<String>? tags,
ProductStatus? status,
AddressModel? address,
}) = _ProductModel;
factory ProductModel.fromJson(Map<String, dynamic> json) =>
_$ProductModelFromJson(json);
factory ProductModel.fromEntity(ProductEntity? entity) =>
_$ProductModelFromEntity(entity);
factory ProductModel.fromHiveObject(ProductObject hiveObject) =>
_$ProductModelFromHiveObject(hiveObject);
}
5. CubitState
// lib/state/product_state.dart
import 'package:flutter_boilerplate_annotations/flutter_boilerplate_annotations.dart';
part 'generated/product_state.g.dart';
@CubitState()
abstract class ProductState {
const factory ProductState.initial() = ProductInitial;
const factory ProductState.loading() = ProductLoading;
const factory ProductState.success({
required List<ProductModel> products,
@Default(0) int total,
}) = ProductSuccess;
const factory ProductState.failure({
required String message,
int? code,
}) = ProductFailure;
}
6. Penggunaan di Cubit
class ProductCubit extends Cubit<ProductState> {
ProductCubit() : super(const ProductState.initial());
Future<void> fetchProducts() async {
emit(const ProductState.loading());
try {
final entities = await repository.getAll();
final models = entities.map(ProductModel.fromEntity).toList();
emit(ProductState.success(products: models, total: models.length));
} catch (e) {
emit(ProductState.failure(message: e.toString()));
}
}
}
7. Penggunaan di Widget
BlocBuilder<ProductCubit, ProductState>(
builder: (context, state) {
return state.when(
initial: () => const SizedBox(),
loading: () => const CircularProgressIndicator(),
success: (products, total) => Column(
children: [
Text('Total: $total'),
...products.map((p) => Text(p.name ?? '')),
],
),
failure: (message, code) => Text('Error $code: $message'),
);
},
)
Referensi Opsi
@Model — semua parameter
| Parameter | Type | Default | Keterangan |
|---|---|---|---|
entity |
Type? |
null |
Domain entity class untuk mapping |
hiveObject |
Type? |
null |
Hive object class untuk mapping |
fromJson |
bool |
true |
Generate _$ClassFromJson() |
toJson |
bool |
true |
Generate _$ClassToJson() + toJson() method |
fromEntity |
bool |
false |
Generate _$ClassFromEntity() |
toEntity |
bool |
true |
Generate _$ClassToEntity() + toEntity() method |
fromHiveObject |
bool |
true |
Generate _$ClassFromHiveObject() |
toHiveObject |
bool |
true |
Generate _$ClassToHiveObject() + toHiveObject() method |
genericAugmented |
bool |
false |
Aktifkan dukungan generic type <T> untuk toJson/fromJson |
@CubitState — semua parameter
| Parameter | Type | Default | Keterangan |
|---|---|---|---|
when |
bool |
true |
Generate .when(...) extension |
maybeWhen |
bool |
true |
Generate .maybeWhen(...) extension |
whenOrNull |
bool |
true |
Generate .whenOrNull(...) extension |
map |
bool |
true |
Generate .map(...) extension |
maybeMap |
bool |
true |
Generate .maybeMap(...) extension |
mapOrNull |
bool |
true |
Generate .mapOrNull(...) extension |
Konstanta siap pakai
// Ekuivalen dengan @Model() tanpa argumen (semua default)
const Model model = Model();
// Ekuivalen dengan @CubitState() tanpa argumen (semua default)
const CubitState cubitState = CubitState();
@model // shorthand untuk @Model()
abstract class SimpleModel with _$SimpleModel { ... }
@cubitState // shorthand untuk @CubitState()
abstract class AppState { ... }
Catatan Penting
Field mapping entity/hive hanya memetakan field yang namanya sama di antara model dan entity/hive object. Field yang hanya ada di satu sisi diabaikan.
Nested model pada toEntity dan toHiveObject otomatis memanggil .toEntity() / .toHiveObject() pada tipe non-primitive. Pastikan nested model juga memiliki method tersebut.
Nested model pada fromEntity memanggil NestedModel.fromEntity(entity.field) — pastikan factory fromEntity pada nested model menerima parameter nullable (EntityType? entity).
Enum di Hive disimpan sebagai String (nama field enum atau nilai @EnumValue) dan di-restore via lookup. Tidak membutuhkan Hive adapter untuk enum.
Null fields di JSON: field nullable yang bernilai null tidak dimasukkan ke map hasil toJson. Saat fromJson, field yang tidak ada di JSON akan bernilai null.