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şma | Necə işləyir | Test |
|---|---|---|
| `package:provider` (rəsmi case study-nin seçimi) | `MultiProvider` kökdə; `context.read()` ilə konstruktora ötürülür | Widget testində ağac əl ilə qurulur |
| Riverpod | Provider-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ə verilir | Widget testində `RepositoryProvider.value` ilə fake |
| `get_it` (service locator) | Qlobal registry: `GetIt.I<Repo>()`; `BuildContext` lazım deyil | Testdə registry sıfırlanmalıdır, əks halda state sızır |
// 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.
// 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
- Case study: dependency injectionrəsmidocs.flutter.dev
Google komandalarının package:provider tövsiyəsi və konstruktor inyeksiyası nümunələri.
- Flutter arxitektura bələdçisirəsmidocs.flutter.dev
- Riverpod: override-larrəsmiriverpod.dev
- package:get_itrəsmipub.dev
Service locator yanaşması — BuildContext-siz giriş lazım olduğu hallar üçün.