Flutter'da Dependency Injection: GetIt ve Injectable
Dependency Injection (DI), bağımlılıkları hardcode etmek yerine dışarıdan vermek anlamına gelir. Bu sayede test edilebilirlik artar, sınıflar birbirinden bağımsız hale gelir. Flutter'da en yaygın yaklaşım GetIt (Service Locator) paketidir; Injectable ile birlikte kullanılınca kayıt kodu otomatik üretilir.
GetIt'i Kurmak
// pubspec.yaml
// dependencies:
// get_it: ^7.7.0
// injectable: ^2.4.4
// dev_dependencies:
// injectable_generator: ^2.6.2
// build_runner: ^2.4.8
// lib/injection.dart
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'injection.config.dart'; // build_runner ile üretilir
final getIt = GetIt.instance;
@InjectableInit(
initializerName: 'init',
preferRelativeImports: true,
asExtension: true,
)
Future<void> configureDependencies() => getIt.init();
// lib/main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await configureDependencies();
runApp(const MyApp());
}
Servis Kaydetmek: Singleton, LazySingleton ve Factory
// Singleton: uygulama başlangıcında bir kez oluşturulur
@singleton
class AnalyticsService {
void logEvent(String name, {Map<String, dynamic>? params}) {
// Firebase Analytics çağrısı
debugPrint('Event: $name, params: $params');
}
}
// LazySingleton: ilk kullanımda oluşturulur, sonra aynı instance döner
@lazySingleton
class ApiClient {
final Dio _dio;
ApiClient() : _dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
)) {
_dio.interceptors.add(LogInterceptor(responseBody: false));
}
Future<Response<T>> get<T>(String path) => _dio.get<T>(path);
Future<Response<T>> post<T>(String path, {dynamic data}) =>
_dio.post<T>(path, data: data);
}
// Factory: her inject'te yeni instance oluşturulur
@injectable
class ProductRepository {
final ApiClient _client;
ProductRepository(this._client); // GetIt otomatik inject eder
Future<List<Product>> getProducts() async {
final response = await _client.get<List<dynamic>>('/products');
return response.data!.map((e) => Product.fromJson(e)).toList();
}
}
Environment'a Göre Farklı Implementasyon
// Arayüz (Domain katmanı)
abstract class AuthRepository {
Future<User> login({required String email, required String password});
Future<void> logout();
Stream<User?> get authStateChanges;
}
// Production implementasyonu
@LazySingleton(as: AuthRepository, env: [Environment.prod])
class FirebaseAuthRepository implements AuthRepository {
final FirebaseAuth _auth = FirebaseAuth.instance;
@override
Future<User> login({required String email, required String password}) async {
final credential = await _auth.signInWithEmailAndPassword(
email: email, password: password);
return User.fromFirebase(credential.user!);
}
@override
Future<void> logout() => _auth.signOut();
@override
Stream<User?> get authStateChanges =>
_auth.authStateChanges().map((u) => u != null ? User.fromFirebase(u) : null);
}
// Test/dev implementasyonu
@LazySingleton(as: AuthRepository, env: [Environment.test, Environment.dev])
class MockAuthRepository implements AuthRepository {
@override
Future<User> login({required String email, required String password}) async {
await Future.delayed(const Duration(milliseconds: 300));
if (email == 'error@test.com') throw const AuthException('Test hatası');
return User(id: 'mock-1', email: email, name: 'Mock Kullanıcı');
}
@override
Future<void> logout() async {}
@override
Stream<User?> get authStateChanges => Stream.value(null);
}
// main.dart — environment seçimi
@InjectableInit(preferRelativeImports: true)
Future<void> configureDependencies({String env = Environment.prod}) =>
getIt.init(environment: env);
// Dev modda çalıştırma
await configureDependencies(env: Environment.dev);
Modüller: Üçüncü Taraf Bağımlılıkları Kaydetmek
// Injectable sınıfı olmayan şeyler için Register Modules kullanın
@module
abstract class AppModule {
@lazySingleton
SharedPreferences get prefs => throw UnimplementedError();
// ^^ bu sadece kayıt şablonu — init aşağıda
@preResolve // async factory için
@lazySingleton
Future<SharedPreferences> get sharedPrefs => SharedPreferences.getInstance();
@lazySingleton
Dio get dio => Dio(BaseOptions(baseUrl: Env.apiUrl));
@lazySingleton
FirebaseFirestore get firestore => FirebaseFirestore.instance;
@lazySingleton
FirebaseStorage get storage => FirebaseStorage.instance;
}
// Kod üretimi için:
// dart run build_runner build --delete-conflicting-outputs
Widget ve BLoC'ta Kullanım
// BLoC'a inject
@injectable
class ProductsCubit extends Cubit<ProductsState> {
final ProductRepository _repo;
final AnalyticsService _analytics;
ProductsCubit(this._repo, this._analytics) : super(const ProductsInitial());
Future<void> load() async {
emit(const ProductsLoading());
try {
final products = await _repo.getProducts();
_analytics.logEvent('products_loaded', params: {'count': products.length});
emit(ProductsLoaded(products));
} catch (e) {
emit(ProductsError(e.toString()));
}
}
}
// main.dart veya route'da BLoC sağlama
BlocProvider(
create: (_) => getIt<ProductsCubit>()..load(),
child: const ProductsPage(),
)
// Servis doğrudan kullanımı (UI bağımsız kod)
final analytics = getIt<AnalyticsService>();
analytics.logEvent('app_started');
GetIt + Injectable kombinasyonu Flutter projelerinde DI'nin en pratik çözümüdür. Injectable'ın ürettiği kod, manuel registration hatalarını ortadan kaldırır. Environment desteği sayesinde test ve prod ortamları için farklı implementasyonlar sorunsuz çalışır.