Dependency injection adalah salah satu konsep yang sering terdengar rumit tapi sebenarnya sederhana: alih-alih membuat dependency di dalam kelas yang membutuhkannya, Anda menyuntikkannya dari luar.
Kenapa ini penting? Kelas yang membuat dependency-nya sendiri tidak bisa ditest dengan mudah, tidak bisa dikonfigurasi ulang tanpa mengubah kode, dan menciptakan coupling yang tersembunyi yang sulit dilacak.
Di Flutter, ada beberapa pendekatan DI yang umum digunakan. Artikel ini membahas ketiga yang paling relevan β dan kapan masing-masing tepat digunakan.
Pendekatan 1: Constructor Injection (Tanpa Package Tambahan)
Ini adalah DI paling sederhana dan sering diremehkan karena tidak menggunakan library apapun. Tapi untuk proyek kecil hingga menengah, ini sering lebih dari cukup.
// Repository membutuhkan ApiClient dan LocalDatabase
class ArtikelRepository {
final ApiClient _apiClient;
final LocalDatabase _database;
// Dependency disuntikkan melalui constructor
const ArtikelRepository({
required ApiClient apiClient,
required LocalDatabase database,
}) : _apiClient = apiClient,
_database = database;
Future<List<Artikel>> getDaftarArtikel() async {
// Gunakan _apiClient dan _database di sini
}
}
// Di main.dart atau root widget:
void main() {
final apiClient = ApiClient(baseUrl: 'https://api.contoh.com');
final database = LocalDatabase();
final repository = ArtikelRepository(
apiClient: apiClient,
database: database,
);
runApp(MyApp(repository: repository));
}
Testingnya sangat straightforward:
test('repository mengembalikan data dari cache ketika offline', () async {
final mockApi = MockApiClient(); // test double
final mockDb = MockLocalDatabase();
when(mockDb.getArtikel()).thenReturn([artikelContoh]);
final repo = ArtikelRepository(
apiClient: mockApi,
database: mockDb,
);
final result = await repo.getDaftarArtikel();
expect(result, [artikelContoh]);
verifyNever(mockApi.get(any)); // pastikan tidak hit network
});
Tidak perlu setup service locator, tidak perlu teardown setelah test, tidak ada global state yang bocor antar test.
Pendekatan 2: get_it β Service Locator
get_it adalah package service locator yang sangat populer di ekosistem Flutter. Ia memungkinkan Anda mendaftarkan dependency di satu tempat dan mengaksesnya dari mana saja tanpa meneruskannya melalui constructor chain.
// setup_dependencies.dart
final getIt = GetIt.instance;
void setupDependencies() {
// Singleton β satu instance untuk seluruh umur aplikasi
getIt.registerSingleton<ApiClient>(
ApiClient(baseUrl: 'https://api.contoh.com'),
);
getIt.registerSingleton<LocalDatabase>(LocalDatabase());
// Lazy singleton β baru dibuat saat pertama diakses
getIt.registerLazySingleton<ArtikelRepository>(() => ArtikelRepository(
apiClient: getIt<ApiClient>(),
database: getIt<LocalDatabase>(),
));
// Factory β instance baru setiap kali diakses
getIt.registerFactory<ArtikelViewModel>(() => ArtikelViewModel(
repository: getIt<ArtikelRepository>(),
));
}
// Di main.dart:
void main() {
setupDependencies();
runApp(const MyApp());
}
// Di widget mana pun:
final repo = getIt<ArtikelRepository>();
get_it sangat nyaman untuk aplikasi yang dependency graph-nya dalam dan kompleks β tidak perlu meneruskan dependency melalui 5 layer widget hanya agar sampai ke widget yang membutuhkannya.
Tapi ada harga yang dibayar: dependency menjadi tersembunyi. Ketika melihat sebuah class, Anda tidak bisa langsung tahu dependency apa yang digunakannya tanpa membaca seluruh kode. Dan testing membutuhkan setup dan reset getIt yang hati-hati agar tidak ada state global yang bocor antar test.
Masalah yang Sering Muncul dengan get_it
Setelah beberapa proyek menggunakan get_it, saya menemukan pola masalah yang berulang:
Test isolation yang tidak sempurna. Kalau lupa reset getIt antara test, test bisa saling mempengaruhi karena berbagi state global yang sama. Ini menyebabkan test yang kadang pass kadang fail tergantung urutan eksekusi β mimpi buruk debugging.
// Wajib di setUp dan tearDown setiap test suite yang menggunakan getIt
setUp(() {
getIt.reset();
setupDependenciesForTest(); // versi test dari setup dependencies
});
tearDown(() => getIt.reset());
Akses sebelum registrasi. Kalau ada kode yang mengakses getIt<T>() sebelum dependency tersebut didaftarkan, Anda mendapatkan exception runtime yang kadang sulit dilacak.
Circular dependency yang tidak terdeteksi sampai runtime. Jika A bergantung pada B dan B bergantung pada A (melalui lazy singleton), masalah baru muncul saat salah satunya pertama kali diakses.
Pendekatan 3: Riverpod sebagai DI
Saya sudah membahas Riverpod sebagai state management di artikel lain. Tapi Riverpod sebenarnya juga adalah sistem dependency injection yang sangat kuat β dan ini yang sering tidak disadari.
// Daftarkan dependency sebagai provider
final apiClientProvider = Provider<ApiClient>((ref) {
return ApiClient(baseUrl: 'https://api.contoh.com');
});
final localDatabaseProvider = Provider<LocalDatabase>((ref) {
return LocalDatabase();
});
final artikelRepositoryProvider = Provider<ArtikelRepository>((ref) {
return ArtikelRepository(
apiClient: ref.watch(apiClientProvider),
database: ref.watch(localDatabaseProvider),
);
});
// Gunakan di widget:
class ArtikelPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final repo = ref.watch(artikelRepositoryProvider);
// ...
}
}
Keunggulan Riverpod untuk DI:
- Compile-time safety β tidak ada exception runtime karena dependency belum didaftarkan
- Test isolation sempurna via ProviderContainer tanpa global state
- Override dependency yang sangat elegan untuk testing:
// Di test:
final container = ProviderContainer(
overrides: [
apiClientProvider.overrideWithValue(MockApiClient()),
localDatabaseProvider.overrideWithValue(InMemoryDatabase()),
],
);
final repo = container.read(artikelRepositoryProvider);
// repo sekarang menggunakan mock dan in-memory database
Tidak ada setup global, tidak ada teardown yang bisa lupa, tidak ada test yang mempengaruhi test lain.
Rekomendasi: Kapan Pakai Apa
Setelah menggunakan ketiganya di berbagai proyek, rekomendasi saya:
Constructor injection murni untuk proyek kecil (< 10 layar) atau library/package yang tidak boleh punya dependency ke framework DI apapun.
get_it untuk tim yang sudah familiar dengannya, atau proyek yang tidak menggunakan Riverpod dan dependency graph-nya cukup dalam.
Riverpod untuk proyek baru yang ingin DI dan state management terintegrasi dalam satu sistem yang konsisten.
Yang perlu dihindari: mencampur lebih dari satu pendekatan dalam satu proyek tanpa alasan yang jelas. Konsistensi dalam pendekatan DI jauh lebih penting dari memilih yang "terbaik secara teori".
Kesimpulan
Dependency injection bukan tentang memilih library yang paling populer. Ini tentang membuat kode Anda lebih mudah ditest, lebih mudah diubah, dan lebih mudah dipahami oleh developer lain β atau diri sendiri enam bulan kemudian.
Mulai sederhana dengan constructor injection. Tambahkan kompleksitas hanya ketika betul-betul dibutuhkan, bukan karena terlihat keren di artikel tutorial.
Belum ada komentar. Jadilah yang pertama menulis.
Tulis Komentar