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.