Sparround

Dependency injection və test edilə bilənlik

State management və DI tez-tez qarışdırılır, halbuki bunlar iki fərqli məsələdir:

  • DI — obyektin asılılıqlarını kim verir (repository view model-ə necə çatır).
  • State management — dəyişikliyin UI-a necə çatdırılması.

Flutter-in rəsmi arxitektura case study-si DI üçün konkret mövqe bildirir: Compass tətbiqində dependency injection `package:provider` ilə həyata keçirilir; Google komandaları Flutter tətbiqləri qurma təcrübəsinə əsaslanaraq DI üçün `package:provider` istifadəsini tövsiyə edir.

Əsas pattern konstruktor vasitəsilə inyeksiyadır: service repository-yə, repository view model-ə konstruktordan verilir; asılılıqlar private saxlanılır ki, view onları view model-in üzərindən keçərək istifadə edə bilməsin.

YanaşmaNecə işləyirTest
`package:provider` (rəsmi case study-nin seçimi)`MultiProvider` kökdə; `context.read()` ilə konstruktora ötürülürWidget testində ağac əl ilə qurulur
RiverpodProvider-lər bir-birini `ref.watch` ilə oxuyur`overrides` ilə istənilən asılılıq bir sətirdə əvəz olunur
`RepositoryProvider` (flutter_bloc)Daxildə package:provider; bloc-lara `context.read()` ilə verilirWidget testində `RepositoryProvider.value` ilə fake
`get_it` (service locator)Qlobal registry: `GetIt.I<Repo>()`; `BuildContext` lazım deyilTestdə registry sıfırlanmalıdır, əks halda state sızır
dart
// Service repository-yə konstruktordan verilir.
class BookingRepository {
  BookingRepository({required ApiClient apiClient}) : _apiClient = apiClient;
  final ApiClient _apiClient; // private: kənardan görünmür
}

// View model repository-ni konstruktordan alır və private saxlayır,
// belə ki, view onun üzərindən keçib repository-yə müraciət edə bilmir.
class HomeViewModel extends ChangeNotifier {
  HomeViewModel({required BookingRepository bookingRepository})
      : _bookingRepository = bookingRepository;
  final BookingRepository _bookingRepository;
}

// Kökdə qurulma (case study-nin nümunəsi ilə eyni struktur).
void main() {
  runApp(
    MultiProvider(
      providers: [
        Provider(create: (context) => ApiClient()),
        Provider(create: (context) => SharedPreferencesService()),
        Provider(
          create: (context) => BookingRepository(apiClient: context.read()),
        ),
      ],
      child: const MainApp(),
    ),
  );
}

Rəsmi case study-nin üslubu: konstruktor inyeksiyası + private sahələr.

dart
// Provider: ağacı testdə əl ilə qurmaq.
await tester.pumpWidget(
  MultiProvider(
    providers: [
      Provider<BookingRepository>.value(value: FakeBookingRepository()),
    ],
    child: const MaterialApp(home: HomeScreen()),
  ),
);

// Riverpod: bir sətirlik override.
await tester.pumpWidget(
  ProviderScope(
    overrides: [
      bookingRepositoryProvider.overrideWithValue(FakeBookingRepository()),
    ],
    child: const MaterialApp(home: HomeScreen()),
  ),
);

// flutter_bloc: repository fake, bloc real.
await tester.pumpWidget(
  RepositoryProvider<BookingRepository>.value(
    value: FakeBookingRepository(),
    child: MaterialApp(
      home: BlocProvider(
        create: (context) => BookingBloc(context.read()),
        child: const HomeScreen(),
      ),
    ),
  ),
);

Eyni asılılığı testdə əvəz etmək — üç library, üç üsul.

get_it kimi service locator-un əsas riski testlər arasında state sızmasıdır: registry qlobaldır, ona görə hər testdə reset çağırılmalıdır və unudulduqda uğursuzluq testlərin sırasından asılı olur. İkinci risk — asılılıqların görünməzliyi: sinifin konstruktoruna baxıb nədən asılı olduğunu anlamaq mümkün olmur, çünki asılılıq metodun içində GetIt.I<T>() ilə alınır. Buna görə mən locator-u yalnız BuildContext-in əlçatan olmadığı yerlərdə (background isolate, entry point) istifadə edir, əsas axında konstruktor inyeksiyasını saxlayıram.

📚 Mənbələr və sənədlər