Table of Contents

Flutter Clean Architecture & TDD(Test Driven Development)

Pengenalan

Arsitektur sangat penting dalam mengembangkan suatu aplikasi. Arsitektur dapat diibaratkan sebuah denah yang menggambarkan bagaimana alur dalam suatu proyek aplikasi. Tujuan utama penerapan arsitektur adalah separation of concern (SoC). Jadi, akan lebih mudah jika kita bisa bekerja dengan fokus pada satu hal dalam satu waktu.

Dalam konteks Flutter, clean architecture akan membantu kita memisahkan kode untuk logika bisnis dengan kode yang terkait dengan platform seperti UI, manajemen negara, dan sumber data eksternal. Selain itu, kode yang kita tulis bisa lebih mudah untuk diuji (testable) secara mandiri.

Berdasarkan pada diagram di atas, clean architecture digambarkan sebagai piramida atau union slice jika dilihat dari atas. Clean Architecture akan membagi proyek Flutter menjadi 3 lapisan utama, yaitu:

Data & Platform layer

Data Layer terletak pada lapisan terluar. Lapisan ini terdiri dari kode sumber data seperti konsumsi Rest API, akses ke database lokal, Firebase, atau sumber lainnya. Selain itu, pada lapisan ini, biasanya terdapat kode platform yang membangun UI (widget).

Presentation Layer

Presentation Layer terdiri dari kode untuk mengakses data aplikasi dari repositori. Juga ada kode untuk state management seperti Provider, BLoC, Getx Controller dan sejenisnya.

Domain Layer

Domain Layer merupakan lapisan terdalam dalam clean architecture. Lapisan ini berisi kode untuk aplikasi logika bisnis seperti entity dan use case.

Setiap lapisan bergantung pada lapisan lainnya. Panah pada diagram menunjukkan bagaimana lapisan-lapisan tersebut saling berhubungan. Lapisan terluar akan bergantung pada lapisan dalam dan seterusnya.

Layer yang tidak bergantung pada layer lain disini hanyalah Domain Layer (independen) yang merupakan kode logika bisnis. Dengan begitu, aplikasi lebih mudah beradaptasi dan dinamis. Misalnya kita ingin mengubah pengelolaan state dari Provider ke BLoC, maka proses migrasi tidak akan mengganggu logika bisnis yang ada.

Test-Driven Development

Selain menerapkan clean architecture, untuk mengoptimalkan proses pengembangan dalam hal menghasilkan minimal bugs dan mengurangi proses debugging dan perbaikan yang berulang, kita harus menjalani proses pengujian.

Test-Driven Development merupakan suatu proses pengembangan aplikasi dimana pengujian merupakan hal utama yang mendorong pengembangan tersebut. Skenario pengujian kode akan ditulis terlebih dahulu sebelum membuat fitur pada aplikasi.

Alur kerja proses pengembangan aplikasi dengan TDD seperti terlihat pada diagram di atas. Perhatikan bahwa proses TDD bersifat berulang dan disebut proses Red-Green-Refactor.

Langkah 1: Membuat Sekenario Test (Red)

Pengembangan suatu fitur diawali dengan penulisan skenario pengujian terlebih dahulu. Penulisan skenario pengujian biasanya mengikuti persyaratan fitur dalam dokumen PRD (untuk kasus di perusahaan). Pada skenario pengujian biasanya akan terdapat alur fitur yang dikembangkan seperti menentukan sumber data yang digunakan (misalnya jarak jauh atau lokal), memastikan data yang masuk dari API menghasilkan model yang sesuai, merancang alur keadaan tampilan berdasarkan datanya, dan sebagainya. . Saat pertama kali kita menulis skenario pengujian, kita akan mendapatkan kesalahan. Hal ini wajar karena kode fiturnya belum ada.tikan Anda memiliki perbaikan dan fitur terbaru, gunakan paket yang dikelola proyek untuk menginstal Jenkins.

Penulisan skenario pengujian suatu fitur dilakukan sebagai panduan dalam mengembangkan fitur tersebut.

Langkah 2 : Membuat sekenario Testing berhasil (Green)

Pada langkah ini, penulisan actual feature code sudah selesai. Penulisan kode pada tahap ini tidak harus rapi dan maksimal karena tujuan utama pada langkah ini adalah membuat kode pengujian yang telah dibuat sebelumnya berhasil.

Langkah 3: Merapihkan & Mengoptimasi Kode

Setelah kode pengujian berhasil dijalankan tanpa ada kesalahan, maka sekarang saatnya merapikan dan mengoptimasi kode yang telah ditulis, baik kode pengujian maupun kode fitur sebenarnya.

Dengan proses TDD, hasil akhir yang akan didapat selain aplikasi yang minim bug, juga kode aplikasi yang rapi dan optimal.

Implementasi

Sekarang kita akan mencoba mengimplementasikan 2 konsep yang telah kita bicarakan sebelumnya dalam sebuah mini proyek yaitu aplikasi cuaca. Kali ini kita akan menggunakan OpenWeather API sebagai sumber data aplikasi kita.

Struktur folder proyek ini akan terlihat seperti ini:

				
					lib
├── data
│   ├── constants.dart
│   ├── datasources
│   │   └── remote_data_source.dart
│   ├── exception.dart
│   ├── failure.dart
│   ├── models
│   │   └── weather_model.dart
│   └── repositories
│       └── weather_repository_impl.dart
├── domain
│   ├── entities
│   │   └── weather.dart
│   ├── repositories
│   │   └── weather_repository.dart
│   └── usecases
│       └── get_current_weather.dart
├── injection.dart
├── main.dart
└── presentation
    ├── bloc
    │   ├── weather_bloc.dart
    │   ├── weather_event.dart
    │   └── weather_state.dart
    └── pages
        └── weather_page.dart

test
├── data
│   ├── datasources
│   │   └── remote_data_source_test.dart
│   ├── models
│   │   └── weather_model_test.dart
│   └── repositories
│       └── weather_repository_impl_test.dart
├── domain
│   └── usecases
│       └── get_current_weather_test.dart
├── helpers
│   ├── dummy_data
│   │   └── dummy_weather_response.json
│   ├── json_reader.dart
│   ├── test_helper.dart
│   └── test_helper.mocks.dart
└── presentation
    ├── bloc
    │   └── weather_bloc_test.dart
    └── pages
        └── weather_page_test.dart
				
			

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut elit tellus, luctus nec ullamcorper mattis, pulvinar dapibus leo.

Langkah 1: Menulis kode pada Layer Domain

Langkah pertama adalah menulis kode pada lapisan domain. Mengapa lapisan domain? karena lapisan ini merupakan lapisan yang tidak bergantung pada lapisan lainnya. Jadi akan lebih aman jika memulai dari layer ini.

Usecases

Di bagian test/domain, kita hanya perlu menulis skenario pengujian untuk kasus penggunaan. Dalam hal ini, kami memiliki 1 usecase yaitu get_current_weather_test.dart.

				
					import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_weather_app_sample/domain/entities/weather.dart';
import 'package:flutter_weather_app_sample/domain/usecases/get_current_weather.dart';
import 'package:mockito/mockito.dart';

import '../../helpers/test_helper.mocks.dart';

void main() {
  late MockWeatherRepository mockWeatherRepository;
  late GetCurrentWeather usecase;

  setUp(() {
    mockWeatherRepository = MockWeatherRepository();
    usecase = GetCurrentWeather(mockWeatherRepository);
  });

  const testWeatherDetail = Weather(
    cityName: 'Jakarta',
    main: 'Clouds',
    description: 'few clouds',
    iconCode: '02d',
    temperature: 302.28,
    pressure: 1009,
    humidity: 70,
  );

  const tCityName = 'Jakarta';

  test(
    'should get current weather detail from the repository',
    () async {
      // arrange
      when(mockWeatherRepository.getCurrentWeather(tCityName))
          .thenAnswer((_) async => const Right(testWeatherDetail));

      // act
      final result = await usecase.execute(tCityName);

      // assert
      expect(result, equals(Right(testWeatherDetail)));
    },
  );
}
				
			

Kode pengujian di atas akan mengalami kesalahan di awal. Ini normal karena kami belum menulis kode sebenarnya.

Untuk kode fitur sebenarnya pada lapisan domain terdapat 3 bagian yaitu entitas, use case, dan repositori. Kita mulai dengan menulis entitas cuaca yaitu Weather.dart

				
					import 'package:equatable/equatable.dart';

class Weather extends Equatable {
  const Weather({
    required this.cityName,
    required this.main,
    required this.description,
    required this.iconCode,
    required this.temperature,
    required this.pressure,
    required this.humidity,
  });

  final String cityName;
  final String main;
  final String description;
  final String iconCode;
  final double temperature;
  final int pressure;
  final int humidity;

  @override
  List<Object?> get props => [
        cityName,
        main,
        description,
        iconCode,
        temperature,
        pressure,
        humidity,
      ];
}
				
			

Setelah itu kita lanjutkan dengan menulis kode untuk weather_repository.dart. Repositori cuaca adalah kelas abstrak dan nantinya akan diimplementasikan pada Layer data.

				
					import 'package:dartz/dartz.dart';
import 'package:flutter_weather_app_sample/data/failure.dart';
import 'package:flutter_weather_app_sample/domain/entities/weather.dart';

abstract class WeatherRepository {
  Future<Either<Failure, Weather>> getCurrentWeather(String cityName);
}