Halo Developer! 👋
Di artikel sebelumnya kita sudah belajar Repository Pattern untuk memisahkan logic dari UI. Sekarang waktunya lanjut ke topik yang saling berkaitan erat: Dependency Injection (DI).
Ever since kita pakai Repository Pattern, mungkin kamu sadar ada masalah baru — bagaimana caranya HomePage dapat akses ke UserRepository tanpa harus tahu detail implementasinya? Di sinilah Dependency Injection hadir sebagai solusi.
Dependency Injection Itu Apa?
Secara sederhana, Dependency Injection adalah mengirim dependensi (objek yang dibutuhkan) dari luar alih-alih membuatnya sendiri di dalam kelas.
Tanpa DI:
class HomePage extends StatefulWidget {
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// HomePage harus tahu cara membuat UserRepository
final _repo = UserRepository(
apiClient: ApiClient(baseUrl: 'https://api.example.com'),
);
@override
Widget build(BuildContext context) {
// ...
}
}
Masalahnya? HomePage harus tahu BaseUrl, harus tahu cara bikin ApiClient, harus tahu cara bikin UserRepository. Kalau kita ganti base URL, semua halaman yang pakai UserRepository harus diubah.
Dengan DI:
class HomePage extends StatefulWidget {
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// Ambil dari "service locator" — tidak peduli bagaimana dibuat
final _repo = getIt<UserRepository>();
@override
Widget build(BuildContext context) {
// ...
}
}
HomePage tinggal ambil UserRepository yang sudah siap pakai. Mau ganti base URL? Cukup ubah di satu tempat saja.
Kenapa pakai get_it?
Flutter sudah punya InheritedWidget dan sebenarnya state management seperti Provider juga bisa jadi DI container. Tapi get_it punya kelebihan:
- Simple — Hanya
registerdanget, tidak ada boilerplate Widget - Bisa dipakai di mana saja — Bukan hanya di Widget tree, tapi juga di service layer, utility, atau main()
- Tidak perlu context — Tidak seperti Provider yang butuh
Provider.of(context) - Singleton & Lazy Singleton — Memastikan hanya ada satu instance (hemat memory)
- Ringan — Package-nya sangat kecil, cocok untuk semua skala project
Setup get_it
1. Install package
Tambahkan di pubspec.yaml:
dependencies:
get_it: ^8.0.3
Lalu jalankan:
flutter pub get
2. Buat Service Locator file
Buat file baru lib/core/locator.dart:
import 'package:get_it/get_it.dart';
import '../data/repositories/user_repository.dart';
import '../data/services/api_client.dart';
import '../data/services/local_storage.dart';
import '../blocs/user/user_cubit.dart';
final getIt = Get.instance;
void setupLocator() {
// Services — dependensi "paling dasar"
getIt.registerLazySingleton<ApiClient>(
() => ApiClient(baseUrl: 'https://api.example.com'),
);
getIt.registerLazySingleton<LocalStorage>(
() => LocalStorage(),
);
// Repository — butuh Services
getIt.registerLazySingleton<UserRepository>(
() => UserRepository(
apiClient: getIt<ApiClient>(),
storage: getIt<LocalStorage>(),
),
);
// Cubit/BLoC — butuh Repository
getIt.registerFactory<UserCubit>(
() => UserCubit(repository: getIt<UserRepository>()),
);
}
3. Panggil di main()
import 'core/locator.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
setupLocator();
runApp(const MyApp());
}
Sekarang di mana saja di dalam app, kamu bisa ambil dependensi dengan getIt<T>().
Tipe Registrasi di get_it
get_it menyediakan beberapa cara mendaftarkan service:
Lazy Singleton
getIt.registerLazySingleton<ApiClient>(
() => ApiClient(baseUrl: 'https://api.example.com'),
);
Objek dibuat saat pertama kali diakses (lazy), dan hanya satu instance (singleton). Cocok untuk service yang mahal dibuat tapi tidak harus langsung dibuat saat app start.
Singleton
getIt.registerSingleton<LocalStorage>(
LocalStorage()..init(),
);
Objek dibuat langsung saat registrasi dan hanya ada satu instance. Cocok untuk service yang harus langsung siap (misal LocalStorage yang perlu init() async — tapi kalau async, pakai registerSingletonAsync).
Factory
getIt.registerFactory<UserCubit>(
() => UserCubit(repository: getIt<UserRepository>()),
);
Objek dibuat baru setiap kali diakses. Cocok untuk Cubit/BLoC karena tiap halaman butuh Cubit yang terpisah.
Perbandingan singkat
| Tipe | Kapan dibuat | Jumlah instance |
|---|---|---|
registerSingleton |
Saat registrasi | 1 |
registerLazySingleton |
Saat pertama kali diakses | 1 |
registerFactory |
Setiap kali diakses | Banyak (baru tiap kali) |
Contoh Lengkap: UserCubit dengan DI
Mari kita lihat bagaimana DI mengalir dari data layer sampai UI.
UserCubit
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../data/repositories/user_repository.dart';
class UserCubit extends Cubit<UserState> {
final UserRepository _repository;
UserCubit({required UserRepository repository})
: _repository = repository,
super(UserInitial());
Future<void> loadUsers() async {
emit(UserLoading());
try {
final users = await _repository.getUsers();
emit(UserLoaded(users));
} catch (e) {
emit(UserError(e.toString()));
}
}
}
Tidak ada import ApiClient, tidak ada import LocalStorage. UserCubit hanya peduli pada UserRepository. Inilah kekuatan DI — loose coupling.
Menggunakan di Widget
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import '../../blocs/user/user_cubit.dart';
class UserListPage extends StatelessWidget {
const UserListPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => getIt<UserCubit>()..loadUsers(),
child: Scaffold(
appBar: AppBar(title: const Text('Users')),
body: BlocBuilder<UserCubit, UserState>(
builder: (context, state) {
if (state is UserLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is UserError) {
return Center(child: Text('Error: ${state.message}'));
}
if (state is UserLoaded) {
return ListView.builder(
itemCount: state.users.length,
itemBuilder: (_, index) {
final user = state.users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
);
},
);
}
return const SizedBox.shrink();
},
),
),
);
}
}
Perhatikan baris create: (_) => getIt<UserCubit>()..loadUsers(). Widget tidak tahu cara UserCubit dibuat — ia hanya meminta ke getIt.
Bonus: Lazy singleton untuk Cubit
Kalau kamu ingin Cubit-nya singleton (shared across app), ganti registrasi:
// Di locator.dart — pakai singleton bukan factory
getIt.registerLazySingleton<UserCubit>(
() => UserCubit(repository: getIt<UserRepository>()),
);
Sekarang di mana pun kamu panggil getIt<UserCubit>(), hasilnya selalu instance yang sama.
Testing dengan DI
Salah satu manfaat terbesar DI: mudah di-test. Untuk unit test, tinggal register mock:
import 'package:flutter_test/flutter_test.dart';
import 'package:get_it/get_it.dart';
import 'package:mockito/mockito.dart';
// Mock Repository
class MockUserRepository extends Mock implements UserRepository {}
void main() {
final getIt = Get.instance;
setUp(() {
// Reset sebelum tiap test
getIt.reset();
// Register mock
getIt.registerLazySingleton<UserRepository>(
() => MockUserRepository(),
);
});
test('loadUsers emits Loaded state', () async {
final mockRepo = getIt<UserRepository>();
when(mockRepo.getUsers()).thenAnswer(
(_) async => [
User(name: 'Ahmad', email: 'ahmad@test.com'),
],
);
final cubit = getIt<UserCubit>();
await cubit.loadUsers();
expect(cubit.state, isA<UserLoaded>());
});
}
UserCubit tidak berubah sedikit pun — kita hanya mengganti UserRepository dengan versi mock. Inilah loose coupling yang dijanjikan DI.
Kapan Harus Pakai DI?
| Situasi | Butuh DI? |
|---|---|
| App sederhana (1-3 halaman) | ❌ Mungkin overkill |
| Multi-page dengan shared service | ✅ Sangat membantu |
| Unit testing | ✅ Membuat testing jauh lebih mudah |
| Banyak service (API, storage, auth, analytics) | ✅ Wajib pakai |
| Bekerja dalam tim / project besar | ✅ Standar industri |
Kesimpulan
Dependency Injection dengan get_it adalah tool sederhana tapi sangat powerful untuk project Flutter:
- Loose coupling — Widget tidak perlu tahu cara dependensi dibuat
- Satu titik registrasi — Ganti implementasi di satu tempat saja
- Siap untuk testing — Tinggal swap dengan mock
- Ringan dan simple — Tidak ada boilerplate berlebih
Di artikel selanjutnya kita akan lanjut ke Error Handling Best Practice — bagaimana menangani error secara elegan di seluruh layer aplikasi Flutter.
Sekarang kamu sudah paham bagaimana menghubungkan semua layer di Flutter tanpa menghardcode dependensi di mana-mana. 🔗
Coba sendiri! Buat project Flutter baru, setup get_it, dan refactor kode yang sebelumnya hardcode dependency. Share ke sosial media dan tag @ahsai001! 🚀