Best Practice #

Menulis kode yang berjalan adalah satu hal — menulis kode yang mudah dibaca, dipelihara, dan diperluas oleh orang lain (termasuk diri sendiri 6 bulan ke depan) adalah hal lain. Google merilis panduan Effective Dart yang merangkum best practice yang dipelajari dari bertahun-tahun membangun Dart dan Flutter. Artikel ini merangkum yang paling penting dan relevan untuk developer Flutter sehari-hari.

Prinsip Dasar: Konsisten dan Ringkas #

Effective Dart dibangun di atas dua tema utama:

BE CONSISTENT (Konsisten)
  Kode yang sama seharusnya terlihat sama.
  Kode yang berbeda seharusnya terlihat berbeda.
  Konsistensi memudahkan otak memproses pola.

BE BRIEF (Ringkas)
  Dart dirancang agar mudah dibaca.
  Jangan tulis panjang jika bisa pendek.
  Tapi jangan sacrifice kejelasan demi keringkasan.

Panduan Effective Dart menggunakan kata kunci: DO (selalu), DON’T (jangan pernah), PREFER (lebih baik), AVOID (hindari), CONSIDER (pertimbangkan).


Konvensi Penamaan #

Penamaan yang konsisten adalah salah satu hal terpenting dalam kode Dart.

Konvensi Dasar #

// UpperCamelCase: class, enum, typedef, type parameter, extension
class UserProfile {}
enum OrderStatus { pending, confirmed, delivered }
typedef Predicate<T> = bool Function(T);
extension StringHelper on String {}

// lowerCamelCase: variabel, fungsi, parameter, method, named constructor
var userName = 'Budi';
void fetchUserData() {}
User.fromJson(Map<String, dynamic> json) {}

// lowercase_with_underscores: library, package, file, direktori
// lib/user_repository.dart
// lib/data/remote/api_client.dart

Penamaan yang Bermakna #

// HINDARI -- nama tidak bermakna
List<dynamic> getData() { ... }
void doStuff(var x) { ... }
final d = DateTime.now();

// LEBIH BAIK -- nama yang menjelaskan tujuan
List<Product> fetchFeaturedProducts() { ... }
void updateCartQuantity(int newQuantity) { ... }
final registrationDate = DateTime.now();

Konvensi Khusus #

// HINDARI awalan 'get' untuk method
// Gunakan getter atau nama deskriptif
// SALAH:
String getName() => _name;
List<User> getUsers() async { ... }

// BENAR:
String get name => _name;          // getter
List<User> fetchUsers() async { ... } // verb yang lebih deskriptif

// HINDARI awalan underscore untuk variabel lokal
// Underscore berarti "private" -- hanya untuk class/top-level member
void process() {
  var _temp = 42;   // SALAH -- membingungkan
  var temp = 42;    // BENAR
}

// GUNAKAN _, __, dst. untuk parameter callback yang tidak dipakai
list.forEach((_) => count++);
map.forEach((_, value) => print(value));

Gunakan Tipe dengan Cerdas #

Prefer Type Inference, tapi Jangan Berlebihan #

// BERLEBIHAN -- tipe sudah obvious dari nilai
Map<String, List<int>> data = <String, List<int>>{};
List<String> names = <String>['Alice', 'Bob'];

// LEBIH BAIK -- biarkan inference bekerja
var data = <String, List<int>>{};
var names = ['Alice', 'Bob'];       // disimpulkan sebagai List<String>

// TAPI -- tulis tipe jika tidak obvious atau untuk kejelasan API publik
List<Product> get featuredProducts => _products.where(...).toList();
final Map<String, UserSettings> _cache = {};

// JANGAN gunakan dynamic kecuali benar-benar perlu
dynamic value = getValue();        // hilangkan type safety
Object value = getValue();         // lebih baik -- masih type-safe

Hindari Casting yang Tidak Perlu #

// SALAH -- casting tidak perlu
final String name = getValue() as String;

// BENAR -- gunakan pattern matching (Dart 3)
if (getValue() case final String name) {
  print(name);
}

// Atau cek tipe dengan is
final value = getValue();
if (value is String) {
  print(value.toUpperCase()); // dipromosikan ke String otomatis
}

Gunakan final dan const Secara Konsisten #

// GUNAKAN const untuk nilai compile-time
const double pi = 3.14159;
const appName = 'MyApp';

// Widget const -- SANGAT PENTING di Flutter untuk performa
const Text('Label Statis')          // BAIK -- tidak di-rebuild
const SizedBox(height: 16)          // BAIK
const EdgeInsets.all(16)            // BAIK

// GUNAKAN final untuk variabel yang tidak berubah setelah init
final user = User(nama: 'Budi');
final DateTime now = DateTime.now();

// HINDARI var untuk variabel yang tidak akan diubah
var pi = 3.14159;  // SALAH -- seharusnya const atau final
var name = 'Budi'; // SALAH jika name tidak akan berubah

Penanganan Error yang Baik #

Gunakan Exception yang Spesifik #

// HINDARI -- terlalu umum, sulit di-handle secara spesifik
throw Exception('Gagal memuat data');
throw Error();

// LEBIH BAIK -- exception yang spesifik dan informatif
class NetworkException implements Exception {
  final String message;
  final int? statusCode;
  NetworkException(this.message, {this.statusCode});

  @override
  String toString() => 'NetworkException: $message (HTTP $statusCode)';
}

class NotFoundException implements Exception {
  final String resource;
  final String id;
  NotFoundException(this.resource, this.id);

  @override
  String toString() => 'NotFoundException: $resource dengan id "$id" tidak ditemukan';
}

// Tangkap exception yang spesifik
try {
  final user = await userRepo.getById(userId);
} on NotFoundException catch (e) {
  showNotFoundError(e.resource);
} on NetworkException catch (e) when (e.statusCode == 401) {
  redirectToLogin();
} on NetworkException catch (e) {
  showNetworkError(e.message);
}

Jangan Tangkap Error Secara Membuta #

// SALAH -- menelan semua error termasuk bug!
try {
  final result = await processData();
  return result;
} catch (e) {
  return null;  // bug tersembunyi, sangat susah di-debug
}

// LEBIH BAIK -- tangkap hanya yang kamu tahu cara menanganinya
try {
  final result = await processData();
  return result;
} on NetworkException {
  return null;  // ini yang kamu harapkan bisa null
  // error lain tetap akan propagate ke atas
}

Async Best Practices #

Jangan Campurkan async/await dengan then() #

// SALAH -- campuran yang membingungkan
Future<void> loadData() async {
  final data = await fetchData();
  return processData(data).then((result) {
    print(result);
  });
}

// BENAR -- konsisten pakai async/await
Future<void> loadData() async {
  final data = await fetchData();
  final result = await processData(data);
  print(result);
}

Jangan Lupakan Error Handling di async #

// SALAH -- unhandled exception bisa crash app
void initData() {
  loadUsers();  // fire and forget -- exception tidak ditangani!
}

// BENAR -- selalu tangani error dari Future
void initData() {
  loadUsers().catchError((error, stack) {
    logger.error('Gagal load users', error, stack);
    showErrorBanner();
  });
}

// Atau jika di dalam fungsi async
Future<void> initData() async {
  try {
    await loadUsers();
  } catch (e, stack) {
    logger.error('Gagal load users', e, stack);
    showErrorBanner();
  }
}

Gunakan unawaited() untuk Fire-and-Forget yang Disengaja #

import 'dart:async';

// SALAH -- warning dari analyzer karena Future tidak di-await
void logEvent(String event) {
  analytics.log(event);  // analyzer warning: unawaited future
}

// BENAR -- eksplisit bahwa ini memang fire-and-forget
void logEvent(String event) {
  unawaited(analytics.log(event));  // tidak ada warning
}

Dokumentasi Kode #

Kapan Menulis Komentar #

// JANGAN komentar yang hanya mengulang kode
// Increment counter
counter++;   // komentar ini tidak berguna sama sekali

// TULIS komentar untuk menjelaskan MENGAPA, bukan APA
// Gunakan delay 100ms untuk memberi waktu animasi selesai
// sebelum navigasi -- tanpa ini transisi terlihat patah
await Future.delayed(const Duration(milliseconds: 100));
Navigator.pop(context);

Doc Comments untuk API Publik #

/// Menghitung total harga keranjang belanja setelah diskon dan pajak.
///
/// [items] adalah daftar item di keranjang.
/// [discountCode] adalah kode promo opsional. Gunakan null jika tidak ada.
/// [taxRate] adalah persentase pajak (0.11 untuk 11%).
///
/// Throws [InvalidDiscountException] jika [discountCode] tidak valid.
/// Returns total harga dalam satuan rupiah (integer, bukan desimal).
///
/// Contoh:
/// ```dart
/// final total = calculateTotal(
///   items: cart.items,
///   discountCode: 'HEMAT10',
///   taxRate: 0.11,
/// );
/// ```
int calculateTotal({
  required List<CartItem> items,
  String? discountCode,
  double taxRate = 0.11,
}) {
  // implementasi...
}

Struktur dan Organisasi Kode #

Urutan Anggota Class #

class UserService {
  // 1. Static fields
  static const int maxRetry = 3;

  // 2. Instance fields (final dulu, lalu mutable)
  final UserRepository _repository;
  final Logger _logger;
  bool _isLoading = false;

  // 3. Constructors
  UserService(this._repository, this._logger);
  UserService.test() : this(MockUserRepository(), MockLogger());

  // 4. Static methods
  static String generateId() => uuid.v4();

  // 5. Getters dan setters
  bool get isLoading => _isLoading;

  // 6. Public methods
  Future<User?> getUser(String id) async { ... }
  Future<void> updateUser(User user) async { ... }

  // 7. Private methods
  Future<User> _fetchFromApi(String id) async { ... }
  void _handleError(Object e) { ... }
}

Urutan Import #

// 1. dart: imports (sorted alphabetically)
import 'dart:async';
import 'dart:convert';
import 'dart:io';

// 2. package: imports (sorted alphabetically)
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:riverpod/riverpod.dart';

// 3. Relative imports (sorted alphabetically)
import '../models/user.dart';
import '../services/auth_service.dart';
import 'user_card.dart';

Pola-pola Idiomatis Dart #

Cascade Operator #

// TANPA cascade -- repetitif
final paint = Paint();
paint.color = Colors.blue;
paint.strokeWidth = 2.0;
paint.style = PaintingStyle.stroke;

// DENGAN cascade -- lebih ringkas
final paint = Paint()
  ..color = Colors.blue
  ..strokeWidth = 2.0
  ..style = PaintingStyle.stroke;

Collection Idioms #

// HINDARI manual loop untuk transformasi sederhana
final hasil = <int>[];
for (final n in angka) {
  if (n.isEven) hasil.add(n * n);
}

// LEBIH BAIK -- functional style
final hasil = angka.where((n) => n.isEven).map((n) => n * n).toList();

// HINDARI pengecekan isEmpty yang tidak perlu
if (list.length == 0) { ... }  // SALAH
if (list.isEmpty) { ... }       // BENAR

if (list.length > 0) { ... }    // SALAH
if (list.isNotEmpty) { ... }    // BENAR

Gunakan Spread dan Collection If/For di Flutter #

// Idiom yang sangat umum di Flutter
Widget build(BuildContext context) {
  return Column(
    children: [
      const HeaderWidget(),
      ...items.map((item) => ItemCard(item: item)),
      if (showFooter) const FooterWidget(),
      if (isLoading) ...[
        const SizedBox(height: 16),
        const CircularProgressIndicator(),
      ],
    ],
  );
}

Tooling: Manfaatkan dart analyze dan dart format #

# Format otomatis seluruh project
dart format lib/ test/

# Analisis kode -- temukan potensi masalah
dart analyze

# Fix masalah yang bisa di-auto-fix
dart fix --apply

# Cek apakah ada masalah (untuk CI)
dart analyze --fatal-infos

Konfigurasi analysis_options.yaml #

# analysis_options.yaml -- letakkan di root project
include: package:flutter_lints/flutter.yaml

analyzer:
  errors:
    # Treat warnings as errors di CI
    missing_return: error
    dead_code: warning

linter:
  rules:
    # Tambahan rule yang berguna
    - always_use_package_imports
    - avoid_dynamic_calls
    - avoid_slow_async_io
    - cancel_subscriptions
    - close_sinks
    - prefer_const_constructors
    - prefer_const_literals_to_create_immutables
    - prefer_final_locals
    - unawaited_futures

Ringkasan #

  • Ikuti konvensi penamaan Dart: UpperCamelCase untuk tipe, lowerCamelCase untuk variabel/fungsi, lowercase_with_underscores untuk file/library.
  • Gunakan final dan const secara konsisten — ini sangat berdampak di Flutter karena const widget tidak di-rebuild.
  • Buat exception yang spesifik dan tangkap hanya yang kamu tahu cara menanganinya — jangan telan semua error.
  • Jangan campurkan async/await dengan .then() — pilih satu gaya dan konsisten.
  • Gunakan unawaited() untuk Future yang memang disengaja fire-and-forget.
  • Tulis doc comments (///) untuk semua API publik — jelaskan parameter, return value, dan exception yang mungkin dilempar.
  • Manfaatkan dart format, dart analyze, dan dart fix secara rutin — idealnya di-integrate ke IDE dan CI/CD pipeline.
  • Ikuti urutan yang konsisten untuk anggota class: static fields → instance fields → constructor → static methods → getters → public methods → private methods.
  • Gunakan cascade operator, collection if/for, dan spread untuk kode yang lebih ringkas dan idiomatis.

← Sebelumnya: Isolate & Concurrency   Berikutnya: Widget Overview →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact