Beranda Coding Clean Code dan Arsitektur Flutter untuk Pemula
CodingFlutter

Clean Code dan Arsitektur Flutter untuk Pemula

Pelajari Clean Code dan arsitektur Flutter, mulai dari SOLID, MVVM, repository pattern, service layer, dependency injection, refactoring, hingga testing aplikasi.

Bagikan
clean code flutter
Bagikan

Membuat aplikasi Flutter yang berjalan dengan baik merupakan langkah penting bagi seorang pemula. Namun, ketika aplikasi mulai memiliki banyak halaman, fitur, integrasi API, database, dan autentikasi, muncul tantangan baru: bagaimana menjaga kode tetap rapi dan mudah dikembangkan?

Pada proyek sederhana, Anda mungkin dapat menulis seluruh logika aplikasi dalam beberapa file. Akan tetapi, pendekatan tersebut akan semakin sulit dipelihara ketika aplikasi memiliki puluhan halaman, berbagai sumber data, dan banyak aturan bisnis.

Perubahan kecil pada satu fitur bahkan dapat menimbulkan masalah pada fitur lain jika seluruh komponen saling bergantung tanpa batas yang jelas.

Karena itulah, Clean Code dan arsitektur aplikasi menjadi keterampilan penting bagi Flutter developer yang ingin naik ke tingkat menengah dan mempersiapkan diri untuk bekerja secara profesional.

Dalam Roadmap Belajar Flutter dari Nol Sampai Siap Kerja, Level 12 merupakan kelanjutan dari Level 11 — Authentication & Backend Flutter. Pada level sebelumnya, Anda telah mempelajari cara membuat login, mengelola sesi, menghubungkan aplikasi dengan backend, dan menerapkan konsep autentikasi.

Sekarang, Anda akan mempelajari bagaimana menyusun semua komponen tersebut agar lebih mudah dibaca, diuji, diperbaiki, dan dikembangkan.

Artikel ini membahas pengertian Clean Code, prinsip SOLID, separation of concerns, struktur folder Flutter, arsitektur berlapis, MVVM, repository pattern, service layer, dependency injection, pengelolaan state, pengujian, hingga contoh refactoring aplikasi Flutter.

Apa Itu Clean Code?

Clean Code adalah pendekatan penulisan kode yang mengutamakan keterbacaan, kejelasan tanggung jawab, konsistensi, dan kemudahan pemeliharaan.

Kode yang bersih bukan berarti kode yang memiliki jumlah baris paling sedikit. Kode yang singkat belum tentu mudah dipahami, sedangkan kode yang panjang belum tentu buruk jika setiap bagian memiliki tujuan yang jelas.

Sebagai contoh, perhatikan dua fungsi berikut.

Contoh pertama:

double hitung(double a, double b) {
  return a - (a * b / 100);
}

Contoh kedua:

double hitungHargaSetelahDiskon({
  required double hargaAwal,
  required double persentaseDiskon,
}) {
  return hargaAwal -
      (hargaAwal * persentaseDiskon / 100);
}

Keduanya menghasilkan perhitungan yang sama. Namun, fungsi kedua lebih mudah dipahami karena nama fungsi dan parameternya menjelaskan tujuan setiap nilai.

Contoh tersebut menunjukkan bahwa keterbacaan merupakan salah satu aspek penting Clean Code pada Flutter.

Mengapa Clean Code penting dalam Flutter?

Ada beberapa manfaat yang akan Anda rasakan ketika menerapkannya.

  • Lebih mudah dibaca: developer lain dapat memahami maksud kode tanpa harus menelusuri seluruh aplikasi.
  • Lebih mudah diperbaiki: kesalahan lebih mudah ditemukan ketika tanggung jawab setiap komponen jelas.
  • Lebih mudah dikembangkan: fitur baru dapat ditambahkan tanpa mengubah terlalu banyak bagian yang tidak berkaitan.
  • Lebih mudah diuji: logika yang dipisahkan dari UI dapat diuji secara independen.
  • Lebih mudah dikerjakan bersama tim: struktur dan konvensi yang konsisten mengurangi kebingungan saat kolaborasi.

Clean Code bukan tujuan yang dicapai dengan sekali refactoring. Ini merupakan kebiasaan yang diterapkan secara berkelanjutan selama pengembangan aplikasi.

Prinsip Dasar Clean Code dalam Dart dan Flutter

Sebelum membahas arsitektur, Anda perlu memahami beberapa kebiasaan dasar yang membuat kode lebih mudah dipelihara.

Gunakan nama yang menjelaskan maksudnya

Nama variabel, fungsi, class, dan file sebaiknya menggambarkan tanggung jawabnya.

Kurang jelas:

final d = 30;
final x = getData();

Lebih jelas:

final timeoutInSeconds = 30;
final employeeList = getEmployees();

Nama singkat tetap dapat digunakan untuk variabel lokal yang sangat sederhana, tetapi hindari nama yang membuat pembaca harus menebak-nebak maksudnya.

Buat fungsi dengan satu tujuan yang jelas

Fungsi yang melakukan terlalu banyak hal akan sulit diuji dan diperbaiki.

Misalnya, fungsi untuk menyimpan transaksi sebaiknya tidak sekaligus menangani validasi formulir, melakukan HTTP request, menghitung laporan, menampilkan dialog, dan mengatur navigasi.

Pisahkan tanggung jawab tersebut ke bagian yang sesuai.

bool isValidAmount(double amount) {
  return amount > 0;
}

double calculateTotal(
  double amount,
  double tax,
) {
  return amount + tax;
}

Contoh ini memperlihatkan dua fungsi dengan tanggung jawab yang berbeda dan jelas.

Hindari duplikasi kode

Jika logika yang sama muncul berkali-kali, pertimbangkan untuk membuat fungsi atau komponen bersama.

Contohnya, apabila beberapa halaman membutuhkan format tanggal yang sama, Anda dapat menyediakan helper atau formatter yang digunakan bersama.

Namun, jangan membuat abstraksi hanya karena dua potongan kode terlihat mirip. Pastikan keduanya memang memiliki perilaku dan tujuan yang sama.

Gunakan konstanta untuk nilai yang bermakna

Hindari angka atau teks yang tersebar tanpa penjelasan ketika nilai tersebut memiliki arti khusus.

const maxRetryAttempts = 3;
const pageSize = 20;

Nama konstanta membantu pembaca memahami tujuan nilai dan mempermudah perubahan di kemudian hari.

Tangani error secara eksplisit

Jangan mengabaikan exception atau menampilkan pesan kesalahan yang tidak membantu.

Pisahkan pesan teknis yang berguna untuk debugging dari pesan yang ditampilkan kepada pengguna. Aplikasi sebaiknya memberikan informasi yang cukup untuk menjelaskan masalah dan tindakan yang dapat dilakukan.

Manfaatkan fitur bahasa Dart

Gunakan null safety, tipe data yang sesuai, parameter bernama, serta class yang memiliki tanggung jawab jelas.

Fitur tersebut membantu mengurangi ambiguitas dan membuat maksud kode lebih eksplisit.

Dokumentasi resmi Dart tentang Effective Dart dapat digunakan sebagai referensi untuk konvensi penulisan kode.

Memahami Separation of Concerns

Separation of concerns adalah prinsip memisahkan bagian aplikasi berdasarkan tanggung jawabnya.

Dalam aplikasi Flutter, logika tampilan sebaiknya tidak bercampur secara berlebihan dengan logika bisnis dan akses data. Widget idealnya berfokus pada bagaimana informasi ditampilkan dan bagaimana pengguna berinteraksi dengan antarmuka.

Sebaliknya, logika pengolahan data, komunikasi API, dan aturan bisnis ditempatkan pada komponen yang sesuai.

Contoh kode yang terlalu bercampur

Bayangkan Anda membuat halaman daftar karyawan yang melakukan semua hal berikut dalam satu widget:

  • Menampilkan daftar karyawan.
  • Memanggil API.
  • Mengubah JSON menjadi objek.
  • Memvalidasi respons server.
  • Mengelola loading dan error.
  • Menghapus data karyawan.
  • Menampilkan dialog konfirmasi.
Baca juga:  Authentication Flutter: Login, Token, dan Backend

Kode seperti ini mungkin tetap berjalan. Masalahnya, setiap perubahan akan semakin sulit dilakukan karena banyak tanggung jawab bercampur dalam satu tempat.

Contoh pemisahan tanggung jawab

Anda dapat membaginya menjadi beberapa bagian.

KomponenTanggung jawab
View atau ScreenMenampilkan UI dan menerima interaksi
ViewModelMengelola state serta logika presentasi
RepositoryMenyediakan dan mengelola akses data aplikasi
ServiceBerkomunikasi dengan API, database, atau sumber data lain
ModelMerepresentasikan data yang digunakan aplikasi

Pembagian ini membantu setiap komponen memiliki batas tanggung jawab yang lebih jelas.

Dokumentasi arsitektur Flutter juga merekomendasikan pemisahan UI layer dan data layer serta penggunaan view, view model, repository, dan service sesuai kebutuhan aplikasi.

Mengenal Prinsip SOLID

SOLID merupakan kumpulan lima prinsip desain perangkat lunak yang membantu membuat kode lebih modular dan mudah dipelihara.

Anda tidak harus menerapkan semua prinsip secara kompleks pada aplikasi sederhana. Namun, memahami konsepnya akan membantu ketika proyek mulai berkembang.

Single Responsibility Principle (SRP)

Satu class sebaiknya memiliki satu tanggung jawab utama.

Contohnya, EmployeeApiService menangani komunikasi dengan endpoint karyawan, sedangkan EmployeeRepository mengatur bagaimana data karyawan disediakan kepada bagian aplikasi lainnya.

Hindari class yang sekaligus mengatur UI, menyimpan data, mengirim request, dan memproses seluruh aturan bisnis.

Open/Closed Principle (OCP)

Komponen sebaiknya dapat diperluas tanpa harus terus-menerus mengubah kode inti yang sudah stabil.

Misalnya, sebuah repository menggunakan kontrak yang sama untuk memperoleh data karyawan. Implementasinya dapat diganti dari API nyata ke data palsu untuk pengujian tanpa mengubah ViewModel yang menggunakannya.

Liskov Substitution Principle (LSP)

Implementasi pengganti harus tetap memenuhi perilaku yang diharapkan dari kontraknya.

Jika dua class sama-sama mengimplementasikan repository karyawan, keduanya harus menyediakan hasil dan perilaku yang konsisten dengan kontrak tersebut.

Interface Segregation Principle (ISP)

Jangan memaksa sebuah class mengimplementasikan banyak fungsi yang tidak dibutuhkannya.

Lebih baik menggunakan kontrak yang terfokus daripada satu interface besar yang memuat terlalu banyak tanggung jawab.

Dependency Inversion Principle (DIP)

Komponen tingkat tinggi sebaiknya tidak bergantung secara langsung pada implementasi teknis tertentu.

Sebagai contoh, ViewModel dapat bergantung pada kontrak EmployeeRepository, bukan langsung membuat objek HTTP client atau memanggil endpoint API.

Dengan demikian, sumber data dapat diganti tanpa harus mengubah logika presentasi secara besar-besaran.

Catatan: SOLID adalah pedoman desain, bukan aturan untuk membuat interface sebanyak mungkin. Terapkan abstraksi ketika memberikan manfaat nyata bagi pengembangan dan pengujian.

Memahami Arsitektur Aplikasi Flutter

Arsitektur aplikasi adalah cara menyusun komponen dan mengatur hubungan antarkomponen agar aplikasi tetap mudah dikembangkan seiring bertambahnya kebutuhan.

Flutter tidak mewajibkan satu pola arsitektur untuk semua aplikasi. Struktur yang cocok untuk aplikasi kalkulator sederhana belum tentu cocok untuk aplikasi bisnis dengan autentikasi, banyak sumber data, dan aturan akses.

Untuk aplikasi yang mulai berkembang, pemisahan UI dan data layer merupakan fondasi yang baik. Lapisan domain atau use case dapat ditambahkan ketika logika bisnis menjadi lebih kompleks.

UI layer

UI layer menangani tampilan dan interaksi pengguna. Komponen yang umum ditemukan antara lain:

  • Screen atau page.
  • Widget yang dapat digunakan kembali.
  • ViewModel atau pengelola state presentasi.

UI layer bertanggung jawab menampilkan kondisi seperti loading, data tersedia, data kosong, dan error.

Data layer

Data layer mengelola akses ke sumber data, seperti REST API Flutter, database lokal, file, atau layanan platform.

Komponen yang umum digunakan:

  • Repository.
  • Service.
  • Data model atau pemetaan respons API.

Dengan pemisahan ini, widget tidak perlu mengetahui detail bagaimana data diperoleh.

Domain layer

Domain layer berisi aturan bisnis atau use case yang memiliki tujuan jelas.

Misalnya, aplikasi keuangan memiliki aturan untuk menghitung total pengeluaran dalam periode tertentu atau menentukan apakah transaksi dapat disimpan.

Lapisan ini bermanfaat jika logika bisnis cukup kompleks, digunakan oleh beberapa fitur, atau perlu diuji secara independen.

Namun, aplikasi CRUD sederhana mungkin tidak memerlukan domain layer tersendiri. Menambahkan terlalu banyak lapisan sejak awal dapat meningkatkan kompleksitas tanpa memberikan manfaat sebanding.

Mengenal MVVM dalam Flutter

MVVM merupakan singkatan dari Model-View-ViewModel. Pola ini memisahkan tampilan, pengelolaan state dan logika presentasi, serta data yang digunakan aplikasi.

Dalam pendekatan arsitektur yang direkomendasikan Flutter, View dan ViewModel berada di UI layer, sedangkan Repository dan Service berada di data layer. Model merepresentasikan data yang digunakan oleh komponen tersebut.

View

View adalah bagian yang menampilkan informasi dan menerima interaksi pengguna. Dalam Flutter, View biasanya tersusun dari satu atau beberapa widget.

Contohnya adalah halaman daftar karyawan yang menampilkan nama, jabatan, dan status setiap karyawan.

ViewModel

ViewModel mengelola state yang diperlukan View dan menyediakan fungsi untuk menjalankan tindakan pengguna.

Misalnya, ViewModel menangani pemuatan daftar karyawan, menyimpan status loading, dan menyediakan pesan error jika pengambilan data gagal.

Model

Model merepresentasikan informasi yang digunakan aplikasi, seperti data karyawan, transaksi, atau produk.

Model dapat menyediakan metode untuk mengubah data mentah menjadi objek Dart dan membantu menjaga tipe data tetap konsisten.

Bagaimana MVVM bekerja?

Alurnya dapat digambarkan sebagai berikut:

Pengguna
   |
   v
View / Screen
   |
   v
ViewModel
   |
   v
Repository
   |
   v
Service
   |
   v
API / Database

Data yang diperoleh dari sumber data kembali melalui repository dan ViewModel, kemudian digunakan View untuk memperbarui tampilan.

Alur tersebut merupakan gambaran sederhana. Implementasi sebenarnya dapat memiliki cache, beberapa repository, atau domain layer jika dibutuhkan.

Untuk penjelasan lebih lanjut, lihat panduan arsitektur Flutter dan dokumentasi konsep arsitektur.

Memahami Service dan Repository Pattern

Service dan repository sering dianggap sama, padahal tanggung jawabnya berbeda.

Service

Service mengisolasi komunikasi teknis dengan sumber data.

Contohnya:

  • Mengirim HTTP request.
  • Membaca data dari API.
  • Berinteraksi dengan database.
  • Mengakses layanan platform.

Service sebaiknya tidak bertanggung jawab atas tampilan atau status loading pada halaman.

Repository

Repository menyediakan sumber data yang dapat digunakan oleh bagian aplikasi lainnya. Repository juga dapat menangani pemetaan data, cache, kebijakan retry, dan pemilihan sumber data sesuai kebutuhan.

Sebagai contoh, EmployeeRepository dapat menggunakan EmployeeApiService untuk mengambil data dari server, lalu mengembalikan daftar objek Employee kepada ViewModel.

Perbedaan sederhananya adalah service berfokus pada interaksi teknis, sedangkan repository berfokus pada penyediaan dan pengelolaan data aplikasi.

Baca juga:  PHP vs JavaScript: Perbedaan dan Kapan Menggunakannya

Contoh Implementasi Arsitektur Flutter

Agar lebih mudah dipahami, kita akan menggunakan contoh aplikasi manajemen karyawan yang mengambil data dari backend.

Langkah pertama: membuat model

Buat file employee.dart.

class Employee {
  const Employee({
    required this.id,
    required this.name,
    required this.position,
  });

  final int id;
  final String name;
  final String position;

  factory Employee.fromJson(
    Map<String, dynamic> json,
  ) {
    return Employee(
      id: json['id'] as int,
      name: json['name'] as String,
      position: json['position'] as String,
    );
  }
}

Class ini merepresentasikan data karyawan dan menyediakan factory constructor untuk mengubah JSON menjadi objek Dart.

Contoh tersebut mengasumsikan bahwa respons API menggunakan tipe data yang sesuai. Pada aplikasi nyata, Anda perlu menangani respons yang tidak lengkap atau tipe data yang berbeda dari perkiraan.

Langkah kedua: membuat service

Buat service yang menangani komunikasi dengan API.

import 'dart:convert';

import 'package:http/http.dart' as http;

import 'employee.dart';

class EmployeeApiService {
  EmployeeApiService(this.client);

  final http.Client client;

  Future<List<Employee>> fetchEmployees() async {
    final uri = Uri.parse(
      'https://example.com/api/employees',
    );

    final response = await client.get(uri);

    if (response.statusCode != 200) {
      throw Exception(
        'Gagal mengambil data karyawan',
      );
    }

    final decoded = jsonDecode(response.body)
        as List<dynamic>;

    return decoded
        .map(
          (item) => Employee.fromJson(
            item as Map<String, dynamic>,
          ),
        )
        .toList();
  }
}

Alamat https://example.com/api/employees hanyalah placeholder. Ganti dengan endpoint API yang benar dan sesuaikan cara parsing dengan format respons backend Anda.

Contoh ini juga menggunakan http.Client yang diberikan dari luar. Pola tersebut memudahkan penggantian client saat pengujian dan mengurangi ketergantungan langsung pada implementasi tertentu.

Tambahkan paket HTTP melalui terminal jika belum tersedia:

flutter pub add http

Langkah ketiga: membuat repository

Repository menjadi perantara antara ViewModel dan service.

import 'employee.dart';
import 'employee_api_service.dart';

class EmployeeRepository {
  EmployeeRepository(this.apiService);

  final EmployeeApiService apiService;

  Future<List<Employee>> getEmployees() {
    return apiService.fetchEmployees();
  }
}

Pada tahap awal, repository ini memang terlihat sederhana. Namun, ketika aplikasi berkembang, repository dapat mengatur cache, pemetaan error, atau pemilihan antara sumber data lokal dan remote tanpa membuat ViewModel bergantung pada detail tersebut.

Langkah keempat: membuat ViewModel

ViewModel bertanggung jawab atas state yang diperlukan halaman. Contoh berikut menggunakan ChangeNotifier agar konsepnya mudah dipahami.

import 'package:flutter/foundation.dart';

import 'employee.dart';
import 'employee_repository.dart';

class EmployeeViewModel extends ChangeNotifier {
  EmployeeViewModel(this.repository);

  final EmployeeRepository repository;

  List<Employee> employees = [];
  bool isLoading = false;
  String? errorMessage;

  Future<void> loadEmployees() async {
    isLoading = true;
    errorMessage = null;
    notifyListeners();

    try {
      employees = await repository.getEmployees();
    } catch (_) {
      errorMessage = 'Data karyawan gagal dimuat.';
    } finally {
      isLoading = false;
      notifyListeners();
    }
  }
}

ViewModel ini mengelola daftar data, status loading, dan pesan error. Ia tidak perlu mengetahui detail widget yang menampilkan data.

Untuk aplikasi produksi, pertimbangkan state yang lebih eksplisit dan pemetaan error yang terstruktur. Contoh di atas sengaja dibuat sederhana agar konsep dasarnya mudah dipahami.

Langkah kelima: menghubungkan komponen

Buat dan hubungkan objek sesuai urutan dependensinya:

final apiService = EmployeeApiService(
  http.Client(),
);

final repository = EmployeeRepository(
  apiService,
);

final viewModel = EmployeeViewModel(
  repository,
);

Contoh ini menunjukkan dependency injection melalui constructor: setiap komponen menerima dependensinya dari luar.

Pada aplikasi yang lebih besar, proses pembuatan objek dapat diatur menggunakan pendekatan dependency injection yang lebih terpusat. Yang terpenting, komponen tidak perlu membuat seluruh dependensinya sendiri di dalam setiap metode.

Mengatur Struktur Folder Flutter

Struktur folder yang baik membantu developer menemukan kode dengan cepat. Tidak ada satu struktur yang wajib digunakan untuk semua aplikasi, tetapi ada beberapa pendekatan yang umum.

Struktur berdasarkan jenis file

Contohnya:

lib/
├── models/
├── screens/
├── widgets/
├── services/
├── repositories/
└── view_models/

Pendekatan ini mudah dipahami untuk aplikasi kecil. Namun, pada aplikasi yang memiliki banyak fitur, file untuk satu fitur dapat tersebar di banyak folder.

Struktur berdasarkan fitur

Contohnya:

lib/
├── core/
│   ├── network/
│   ├── routing/
│   └── utils/
├── features/
│   ├── auth/
│   │   ├── data/
│   │   ├── models/
│   │   └── presentation/
│   ├── employees/
│   │   ├── data/
│   │   ├── models/
│   │   └── presentation/
│   └── profile/
│       ├── data/
│       ├── models/
│       └── presentation/
└── main.dart

Pendekatan ini mengelompokkan file berdasarkan fitur, sehingga komponen yang berkaitan lebih mudah ditemukan.

Untuk aplikasi yang berkembang, Anda dapat menggabungkan struktur berdasarkan fitur dengan pemisahan layer. Sesuaikan struktur tersebut dengan ukuran proyek dan cara kerja tim.

Memahami Dependency Injection

Dependency injection (DI) adalah pendekatan untuk memberikan dependensi kepada suatu komponen dari luar, bukan membuat semua dependensi secara langsung di dalam komponen tersebut.

Misalnya, ViewModel memerlukan repository untuk memperoleh daftar karyawan. Dengan DI, repository diberikan melalui constructor.

class EmployeeViewModel {
  EmployeeViewModel(this.repository);

  final EmployeeRepository repository;
}

Manfaatnya antara lain:

  • Memudahkan penggantian implementasi.
  • Mengurangi ketergantungan langsung antarkomponen.
  • Memudahkan pengujian menggunakan fake atau mock.
  • Memisahkan konfigurasi dependensi dari logika aplikasi.

Untuk aplikasi kecil, constructor injection mungkin sudah cukup. Aplikasi yang lebih besar dapat menggunakan container dependency injection atau pendekatan pengelolaan dependensi lain.

Jangan menambahkan DI framework hanya karena proyek lain menggunakannya. Pahami kebutuhan proyek terlebih dahulu dan pilih pendekatan yang membuat alur dependensi lebih jelas.

Dokumentasi Flutter membahas dependency injection sebagai salah satu rekomendasi penting dalam struktur aplikasi yang dapat dipelihara, lihat rekomendasi arsitektur resmi Flutter.

Mengelola State dengan Arsitektur yang Baik

State management Flutter yang telah dipelajari pada Level 7 tetap penting ketika Anda mulai menerapkan arsitektur aplikasi.

Perbedaannya, pada level ini Anda tidak hanya memilih cara memperbarui UI. Anda juga menentukan di mana state seharusnya dikelola dan komponen mana yang bertanggung jawab atas perubahan data.

Local UI state

State sederhana yang hanya digunakan oleh satu widget dapat tetap dikelola secara lokal menggunakan StatefulWidget dan setState.

Contohnya adalah status apakah password sedang ditampilkan atau disembunyikan.

Feature state

State yang melibatkan proses pemuatan data, validasi, atau beberapa tindakan dalam satu fitur dapat dikelola melalui ViewModel atau solusi state management lain yang sesuai.

Shared application state

State yang dibutuhkan oleh beberapa fitur, seperti sesi pengguna atau preferensi aplikasi, memerlukan pengelolaan yang lebih terstruktur agar tidak terjadi duplikasi sumber data.

Prinsip yang perlu diingat adalah bahwa UI harus mencerminkan state yang benar. Hindari menyimpan salinan data yang sama di banyak tempat tanpa mekanisme sinkronisasi yang jelas.

Jika data karyawan dikelola oleh repository sebagai sumber data utama, fitur yang membutuhkan data tersebut sebaiknya menggunakan sumber yang konsisten, bukan membuat versi data yang saling bertentangan.

Baca juga:  Kelebihan dan Kekurangan PHP yang Perlu Diketahui Pemula

Refactoring: Memperbaiki Kode Tanpa Mengubah Perilakunya

Refactoring adalah proses memperbaiki struktur internal kode tanpa mengubah perilaku eksternal yang diharapkan.

Contohnya, Anda memiliki halaman daftar produk yang berisi kode UI, request API, parsing JSON, dan perhitungan harga. Setelah aplikasi berkembang, Anda memindahkan akses API ke service dan mengelola state melalui ViewModel.

Jika dilakukan dengan benar, pengguna tetap mendapatkan fungsi yang sama, tetapi kode menjadi lebih mudah dipahami dan diuji.

Tahapan refactoring yang aman

  1. Pahami perilaku aplikasi saat ini.
  2. Identifikasi bagian yang memiliki tanggung jawab terlalu banyak.
  3. Pilih satu bagian kecil untuk diperbaiki.
  4. Pisahkan fungsi atau komponen secara bertahap.
  5. Jalankan pemeriksaan kode dan pengujian.
  6. Pastikan hasil aplikasi tetap sesuai kebutuhan.
  7. Lanjutkan ke bagian berikutnya.

Jangan melakukan perubahan besar pada seluruh aplikasi sekaligus jika belum memahami dampaknya.

Refactoring juga bukan alasan untuk menambahkan lapisan yang tidak dibutuhkan. Tujuannya adalah mengurangi kompleksitas dan memperjelas tanggung jawab, bukan sekadar menambah jumlah file.

Menulis Kode yang Mudah Diuji

Arsitektur yang baik membantu pengujian karena komponen memiliki batas tanggung jawab dan dependensi yang jelas.

Misalnya, logika perhitungan total transaksi dapat diuji tanpa harus membuka aplikasi. ViewModel dapat diuji menggunakan repository palsu, sedangkan widget dapat diuji melalui interaksi pengguna.

Flutter membedakan beberapa jenis pengujian:

Jenis pengujianTujuan
Unit testMenguji satu fungsi, class, atau unit logika
Widget testMemeriksa tampilan dan interaksi widget
Integration testMenguji alur aplikasi yang melibatkan beberapa komponen

Sebagai contoh, Anda dapat menulis unit test untuk memastikan perhitungan diskon benar, widget test untuk memeriksa apakah pesan error muncul, dan integration test untuk menguji alur pengguna dari login hingga membuka halaman utama.

Pengujian tidak harus menunggu aplikasi selesai sepenuhnya. Menulis test selama pengembangan membantu menemukan masalah lebih awal.

Baca dokumentasi resmi Flutter tentang testing untuk memahami perbedaan serta penggunaan setiap jenis pengujian.

Kesalahan yang Harus Dihindari saat Menerapkan Arsitektur

Ada beberapa kesalahan umum yang dapat membuat struktur aplikasi justru lebih rumit.

1. Menganggap semakin banyak folder berarti semakin baik. Struktur yang baik adalah struktur yang memudahkan pemahaman, bukan yang memiliki paling banyak lapisan.

2. Menaruh seluruh logika dalam widget. Widget yang menangani tampilan, request API, validasi, dan aturan bisnis sekaligus akan semakin sulit dipelihara.

3. Membuat abstraksi terlalu dini. Interface, base class, dan use case sebaiknya ditambahkan ketika memang membantu mengelola kompleksitas.

4. Menggunakan pola arsitektur hanya karena sedang populer. Pilih pola yang sesuai dengan kebutuhan aplikasi dan kemampuan tim.

5. Mengabaikan pengujian setelah refactoring. Perubahan struktur tetap dapat menimbulkan bug. Jalankan test dan pemeriksaan kode setelah perubahan penting.

6. Membuat banyak sumber data untuk informasi yang sama. Tentukan sumber data utama dan pastikan perubahan tidak membuat state antarfitur saling bertentangan.

7. Menggunakan dependency injection tanpa memahami dependensinya. DI bukan solusi otomatis untuk semua masalah desain. Tujuannya adalah membuat hubungan antarkomponen lebih eksplisit dan fleksibel.

8. Menganggap Clean Architecture harus diterapkan secara penuh pada setiap aplikasi. Tidak semua aplikasi membutuhkan domain layer atau use case tersendiri. Mulailah dari pemisahan yang memberikan manfaat nyata.

Proyek Praktik: Refactoring Aplikasi Manajemen Karyawan

Untuk menerapkan seluruh materi Level 12, gunakan kembali aplikasi manajemen karyawan yang telah diperkenalkan pada Level 11 — Authentication & Backend Flutter.

Pada tahap sebelumnya, aplikasi tersebut telah memiliki login, profil pengguna, daftar karyawan, dan integrasi backend. Sekarang, fokusnya adalah memperbaiki struktur kode.

Target refactoring

  • Memisahkan halaman UI dari logika presentasi.
  • Memindahkan request API ke service.
  • Menggunakan repository sebagai pengelola sumber data.
  • Membuat model karyawan dengan tipe data yang jelas.
  • Mengelola loading, success, dan error secara terstruktur.
  • Menggunakan dependency injection untuk memberikan dependensi.
  • Menulis test untuk logika penting.

Tahapan pengerjaan

Tahap 1 — Audit kode. Temukan halaman yang melakukan terlalu banyak tugas sekaligus. Catat bagian yang paling sulit dibaca, diuji, atau diperbaiki.

Tahap 2 — Pisahkan akses data. Pindahkan HTTP request dan parsing respons ke service yang memiliki tanggung jawab jelas.

Tahap 3 — Buat repository. Sediakan metode yang dapat digunakan ViewModel tanpa bergantung pada detail endpoint API.

Tahap 4 — Pindahkan logika presentasi. Kelola loading, data, dan error dalam ViewModel atau pengelola state yang sesuai.

Tahap 5 — Rapikan UI. Pastikan widget berfokus pada tampilan dan interaksi, bukan mengelola seluruh proses bisnis.

Tahap 6 — Tambahkan pengujian. Uji repository, ViewModel, dan widget sesuai tanggung jawab masing-masing.

Tahap 7 — Evaluasi hasil. Bandingkan kode sebelum dan sesudah refactoring. Periksa apakah tanggung jawab lebih jelas, dependensi lebih mudah diganti, dan pengujian lebih sederhana.

Target hasil akhir

Anda harus memiliki aplikasi yang tetap berfungsi, tetapi memiliki pemisahan tanggung jawab yang lebih jelas dan struktur yang lebih mudah dipelihara.

Sebagai portofolio, dokumentasikan alasan perubahan, diagram arsitektur sederhana, struktur folder, dan contoh pengujian. Hal tersebut membantu menunjukkan bahwa Anda memahami bukan hanya cara membuat aplikasi, tetapi juga cara merawat kode.

Checklist Kelulusan Level 12

Sebelum melanjutkan ke level berikutnya, gunakan checklist berikut untuk mengevaluasi pemahaman Anda.

  • Memahami tujuan Clean Code dan manfaatnya.
  • Mampu memilih nama variabel, fungsi, dan class yang jelas.
  • Memahami separation of concerns.
  • Memahami lima prinsip SOLID secara konseptual.
  • Mampu membedakan UI layer, data layer, dan domain layer.
  • Memahami peran View, ViewModel, Model, Repository, dan Service.
  • Mampu menjelaskan konsep MVVM dalam Flutter.
  • Memahami dependency injection.
  • Mampu memilih struktur folder sesuai kebutuhan proyek.
  • Mampu melakukan refactoring secara bertahap.
  • Memahami cara memisahkan kode agar mudah diuji.
  • Mampu menulis unit test atau widget test dasar.
  • Memiliki proyek yang sudah dirapikan dan didokumentasikan.

Anda tidak harus langsung menguasai seluruh pola arsitektur tingkat lanjut. Yang terpenting adalah memahami tanggung jawab komponen dan mampu menjelaskan mengapa struktur tertentu dipilih untuk proyek Anda.

Setelah memahami Clean Code dan arsitektur, langkah selanjutnya adalah memastikan aplikasi bekerja sesuai kebutuhan dan tetap stabil ketika kode berubah.

Pada Level 13 — Testing & Debugging Flutter, pembelajaran akan berfokus pada cara menemukan kesalahan, menulis unit test, melakukan widget testing, menjalankan integration test, menggunakan Flutter DevTools, dan mengevaluasi perilaku aplikasi.

Level 12 membantu Anda menyusun kode agar lebih mudah diuji. Level 13 mengajarkan cara memanfaatkan pengujian dan debugging untuk memastikan kode tersebut bekerja dengan benar.

Keduanya saling melengkapi: arsitektur yang terstruktur membuat pengujian lebih mudah, sedangkan pengujian membantu menjaga kualitas arsitektur dan implementasi selama pengembangan.

Kesimpulan

Clean Code dan arsitektur Flutter merupakan fondasi penting untuk membangun aplikasi yang lebih terstruktur, mudah dipahami, dan siap dikembangkan.

Pada Level 12, Anda mempelajari cara menulis kode yang jelas, menerapkan separation of concerns, memahami SOLID, mengenal MVVM, memisahkan UI dan data layer, serta menggunakan service, repository, dan dependency injection.

Anda juga belajar bahwa arsitektur bukan sekadar struktur folder. Arsitektur menentukan bagaimana komponen berkomunikasi, bagaimana tanggung jawab dibagi, bagaimana data dikelola, dan seberapa mudah aplikasi diuji.

Mulailah dengan perbaikan sederhana: rapikan nama fungsi, pisahkan request API dari UI, buat repository yang jelas, dan kelola state secara terstruktur. Tambahkan abstraksi atau domain layer ketika kompleksitas aplikasi memang membutuhkannya.

Dengan kebiasaan tersebut, Anda akan lebih siap mengembangkan proyek Flutter yang besar dan berkolaborasi dengan tim pengembang. Setelah itu, lanjutkan ke Level 13 — Testing, Debugging, dan Performance Flutter untuk memperkuat kemampuan menjaga kualitas aplikasi.

Bagikan

Tinggalkan komentar

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *

Artikel Terkait
testing performance flutter
CodingFlutter

Testing, Debugging, dan Performance Flutter untuk Pemula

Pelajari testing, debugging, dan optimasi performa Flutter melalui unit test, widget test,...

authentication flutter
CodingFlutter

Authentication Flutter: Login, Token, dan Backend

Pelajari authentication dan backend Flutter dari nol, mulai dari registrasi, login, logout,...

membuat aplikasi flutter
CodingFlutter

Membuat Aplikasi Flutter: 7 Ide Project untuk Pemula

Pelajari cara membuat aplikasi Flutter nyata melalui 7 proyek, mulai dari To-Do...

database flutter
CodingFlutter

Database Flutter: SQLite, Local Storage, dan CRUD untuk Pemula

Pelajari database Flutter dan local storage dari nol, mulai dari SQLite, shared_preferences,...