diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d5dd5e..05f7992 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.0.0 + +* Big Refactoring + ## 3.0.0 * Rename classes, all test passed, change readme diff --git a/README.md b/README.md index ee63d59..f9a15c7 100644 --- a/README.md +++ b/README.md @@ -1,181 +1,130 @@ +Here’s the translated and updated `README.md` for the `depend` library: -# depend +--- + +# Depend ![Pub Version](https://img.shields.io/pub/v/depend) ![License](https://img.shields.io/github/license/AlexHCJP/depend) ![Coverage](https://img.shields.io/codecov/c/github/contributors-company/depend) ![Stars](https://img.shields.io/github/stars/AlexHCJP/depend) -`depend` is a library for managing dependencies in Flutter applications. It provides a convenient way to initialize and access services or repositories via an `InheritedWidget`. +`depend` is a library for dependency management in Flutter applications. It provides a convenient way to initialize and access services and repositories via `InheritedWidget`. --- -## Why it Rocks 🚀 +## Features 🚀 -- Initialize dependencies before launching the app -- Access dependencies from anywhere in the widget tree -- Clean and extensible way to manage dependencies -- Easy to use and integrate with existing codebases +- **Dependency Initialization:** Prepare all dependencies before the app launches. +- **Global Access:** Access dependencies from anywhere in the widget tree. +- **Parent Dependencies Support:** Easily create nested or connected dependencies. +- **Ease of Use:** Integrate the library into existing code with minimal changes. --- -- **[dependencies](#depend)** - - **[Why it Rocks 🚀](#why-it-rocks-)** - - **[Installation](#installation)** - - **[Example Usage](#example-usage)** - - **[Example 1: Define InjectionScope](#example-1-define-injectionscope)** - - **[Step 2: Initialize InjectionScope](#step-2-initialize-injectionscope)** - - **[Step 3: Access InjectionScope with `InheritedWidget`](#step-3-access-injectionscope-with-inheritedwidget)** - - **[Example 2: Use Parent InjectionScope](#example-2-use-parent-injectionscope)** - - **[Step 1: Define Parent InjectionScope](#step-1-define-parent-injectionscope)** - - **[Migrate v2 to v3](#migrate-from-v2-to-v3)** +## Table of Contents + +- [Installation](#installation) +- [Usage Examples](#usage-examples) + - [Example 1: Simple Initialization](#example-1-simple-initialization) + - [Example 2: Parent Dependencies](#example-2-parent-dependencies) + - [Example 3: DependencyScope](#example-3-dependencyscope) +- [Migration Guide](#migration-guide) + - [From Version 3 to Version 4](#from-version-3-to-version-4) +- [Code Coverage](#code-coverage) --- ## Installation -Add the package to your `pubspec.yaml`: +Add the library to the `pubspec.yaml` of your project: ```yaml dependencies: depend: ^latest_version ``` -Then run: +Install the dependencies: ```bash -$ flutter pub get +flutter pub get ``` + --- -## Example Usage +## Usage Examples -### Example 1: Define InjectionScope +### Example 1: Simple Initialization -#### Step 1: Extends `Injection` +#### Step 1: Define the Dependency -Create a `Injection` that extends `Injection` and initializes your dependencies: +Create a class that extends `DependencyContainer` and initialize your dependencies: ```dart -class RootInjection extends Injection { +class RootDependency extends DependencyContainer { late final ApiService apiService; @override Future init() async { - apiService = await ApiService().init() + apiService = await ApiService().init(); + } + + void dispose() { + // apiService.dispose() } } ``` -#### Step 2: Initialize InjectionScope +#### Step 2: Use `DependencyScope` -Use `InjectionScope` to initialize your dependencies before launching the app: +Wrap your app in a `DependencyScope` to provide dependencies: ```dart void main() { runApp( - InjectionScope( - injection: RootInjection(), - placeholder: const ColoredBox( - color: Colors.white, - child: Center(child: CircularProgressIndicator()), - ), - child: const MyApp(), + DependencyScope( + dependency: RootDependency(), + placeholder: const Center(child: CircularProgressIndicator()), + builder: (BuildContext context) => const MyApp(), ), ); } ``` -#### Step 3: Access InjectionScope with `InheritedWidget` +#### Step 3: Access the Dependency in a Widget -Once initialized, dependencies can be accessed from anywhere in the widget tree using `InjectionScope.of(context).authRepository`: +You can now access the dependency using `DependencyProvider` anywhere in the widget tree: ```dart - -/// The repository for the example -final class AuthRepository { - final AuthDataSource dataSource; - - AuthRepository({required this.dataSource}); - - Future login() => dataSource.login(); - - void dispose() { - // stream.close(); - } -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - home: InjectionScope( - injection: ModuleInjection( - parent: InjectionScope.of(context), - ), - child: BlocProvider( - create: (context) => DefaultBloc( - InjectionScope.of(context).authRepository, - ), - child: const MyHomePage(), - ), - ), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key}); - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - void _login() { - context.read().add(DefaultEvent()); - } - +class MyWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - body: SafeArea( - child: SingleChildScrollView( - child: Column( - children: [ - BlocBuilder( - builder: (context, state) { - return Text('Login: ${state.authorized}'); - }, - ), - Builder( - builder: (context) { - return ElevatedButton( - onPressed: _login, - child: const Text('Login'), - ); - }, - ) - ], - ), - ), - ), + final apiService = DependencyProvider.of(context).apiService; + + return FutureBuilder( + future: apiService.getData(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const CircularProgressIndicator(); + } else if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } + + return Text('Data: ${snapshot.data}'); + }, ); } } - ``` -### Example 2: Use Parent InjectionScope +--- -#### Step 1: Define Parent InjectionScope +### Example 2: Parent Dependencies -```dart +#### Step 1: Create the Parent Dependency -class RootInjection extends Injection { +```dart +class RootDependency extends DependencyContainer { late final ApiService apiService; @override @@ -183,74 +132,76 @@ class RootInjection extends Injection { apiService = await ApiService().init(); } } +``` -class ModuleInjection extends Injection { +#### Step 2: Create the Child Dependency + +Use the parent dependency inside the child: + +```dart +class ModuleDependency extends DependencyContainer { late final AuthRepository authRepository; - ModuleInjection({required super.parent}); + ModuleDependency({required super.parent}); @override Future init() async { - // initialize dependencies authRepository = AuthRepository( - dataSource: AuthDataSource( - apiService: parent.apiService, // parent - RootInjection - ), + apiService: parent.apiService, ); } - - @override - void dispose() { - authRepository.dispose(); - } } - - - ``` -### Migrate from v2 to v3 -In version 2, dependencies were injected using `Dependencies`, but in version 3, this has been replaced by `InjectionScope`. Here's how you would migrate: +#### Step 3: Link Both Dependencies -#### v2: ```dart void main() { runApp( - Dependencies( - library: RootLibrary(), - placeholder: const ColoredBox( - color: Colors.white, - child: Center(child: CircularProgressIndicator()), + DependencyScope( + dependency: RootDependency(), + builder: (BuildContext context) => DependencyScope( + dependency: ModuleDependency( + parent: DependencyProvider.of(context), + // or + // parent: context.depend(), + ), + builder: (BuildContext context) => const MyApp(), ), - child: const MyApp(), ), ); } ``` -#### v3: +--- + +### Example 3: DependencyScope + ```dart -void main() { - runApp( - InjectionScope( - injection: RootInjection(), - placeholder: const ColoredBox( - color: Colors.white, - child: Center(child: CircularProgressIndicator()), - ), - child: const MyApp(), +DependencyScope( + dependency: RootDependency(), + builder: (BuildContext context) => Text('Inject'), + placeholder: Text('Placeholder'), + errorBuilder: (Object? error) => Text('Error'), ), - ); -} ``` -The key change is moving from `Dependencies` to `InjectionScope`, reflecting the updated structure for managing and accessing dependencies. +## Migration Guide ---- +### From Version 3 to Version 4 + +#### Version 3: -### v2: ```dart -class RootLibrary extends DependenciesLibrary { +InjectionScope( + library: RootLibrary(), + placeholder: const Center(child: CircularProgressIndicator()), + child: const YourWidget(), +); +``` + +```dart +class RootInjection extends Injection { late final ApiService apiService; @override @@ -260,20 +211,58 @@ class RootLibrary extends DependenciesLibrary { } ``` -### v3: ```dart -class RootInjection extends Injection { - late final ApiService apiService; +InjectionScope.of(context); +``` + +#### Version 4: + +```dart +DependencyScope( + dependency: RootDependency(), + placeholder: const Center(child: CircularProgressIndicator()), + builder: (context) => const YourWidget(), +); +``` + +```dart +class ModuleDependency extends DependencyContainer { + late final AuthRepository authRepository; + + ModuleDependency({required super.parent}); @override Future init() async { - apiService = await ApiService().init(); + authRepository = AuthRepository( + apiService: parent.apiService, + ); + } + + void dispose() { + // authRepository.dispose(); } } ``` -The primary change here is the renaming of `RootLibrary` to `RootInjection`, aligning with the shift in naming conventions from `DependenciesLibrary` in v2 to `Injection` in v3. +```dart +DependencyProvider.of(context); +DependencyProvider.maybeOf(context); +// or +context.depend(); +context.dependMaybe(); +``` -## Codecov +#### Key Differences: +- `InjectionScope` → `DependencyScope` +- `Injection` → `DependencyContainer` +- `InjectionScope` → `DependencyProvider` + +--- + +## Code Coverage ![Codecov](https://codecov.io/gh/contributors-company/depend/graphs/sunburst.svg?token=DITZJ9E9OM) + +--- + +This version reflects the latest changes and provides clear guidance for new users. \ No newline at end of file diff --git a/coverage/lcov.info b/coverage/lcov.info index bba4f34..6c99493 100644 --- a/coverage/lcov.info +++ b/coverage/lcov.info @@ -1,57 +1,85 @@ -SF:lib/src/injection.dart -DA:31,2 -DA:44,1 -DA:46,1 -DA:47,3 -DA:49,1 -DA:92,1 -LF:6 -LH:6 +SF:lib/src/dependency_container.dart +DA:39,1 +DA:51,1 +DA:53,1 +DA:54,1 +DA:55,2 +DA:56,1 +DA:59,1 +DA:69,2 +DA:99,1 +DA:100,1 +DA:101,1 +DA:102,1 +DA:121,1 +LF:13 +LH:13 end_of_record -SF:lib/src/injection_exception.dart -DA:22,1 -LF:1 -LH:1 +SF:lib/src/context_extension.dart +DA:17,1 +DA:18,1 +DA:34,1 +DA:35,1 +LF:4 +LH:4 +end_of_record +SF:lib/src/dependency_provider.dart +DA:34,2 +DA:39,2 +DA:63,1 +DA:69,1 +DA:70,1 +DA:72,1 +DA:73,1 +DA:92,1 +DA:96,2 +DA:99,1 +DA:100,1 +DA:101,1 +DA:109,1 +DA:111,3 +LF:14 +LH:14 end_of_record -SF:lib/src/injection_scope.dart -DA:48,1 -DA:54,4 -DA:55,3 -DA:56,2 -DA:57,3 -DA:88,1 -DA:94,1 -DA:95,1 +SF:lib/src/dependency_scope.dart +DA:39,1 +DA:61,1 +DA:62,1 +DA:69,1 +DA:71,1 +DA:72,2 +DA:75,1 +DA:77,1 +DA:80,4 +DA:81,2 +DA:82,2 +DA:86,1 +DA:89,3 +DA:90,1 +DA:94,4 +DA:96,1 DA:97,1 DA:98,1 +DA:99,1 +DA:100,1 +DA:102,4 +DA:103,2 +DA:106,1 +DA:108,1 +DA:109,1 +DA:110,1 +DA:111,2 +DA:114,2 +DA:115,2 DA:116,1 -DA:120,2 -DA:122,2 -DA:123,1 -DA:125,1 -DA:126,1 -DA:127,2 -DA:128,1 -DA:129,1 -DA:130,2 -DA:132,1 -DA:133,1 -DA:134,1 -DA:135,1 -DA:136,2 -DA:137,2 -DA:138,1 -DA:139,1 -DA:145,1 -DA:147,3 -DA:151,1 -DA:160,1 -DA:161,1 -DA:166,1 -DA:168,3 -DA:169,1 -DA:172,1 -DA:173,2 -LF:38 -LH:38 +DA:117,2 +LF:31 +LH:31 +end_of_record +SF:lib/src/injection_exception.dart +DA:26,1 +DA:43,1 +DA:45,5 +LF:3 +LH:3 end_of_record diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index d4f964f..c01c581 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -168,7 +168,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 56473fd..bcc668f 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ init() async { - apiService = await ApiService().init(); - } - -} - -class ModuleInjection extends Injection { - late final AuthRepository authRepository; - - ModuleInjection({required super.parent}); - - @override - Future init() async { - authRepository = AuthRepository( - dataSource: AuthDataSource( - apiService: parent.apiService, - ), - ); - } - - @override - void dispose() { - authRepository.dispose(); - } -} - void main() { - runApp( - InjectionScope( - injection: RootInjection(), - placeholder: const ColoredBox( - color: Colors.white, - child: Center(child: CircularProgressIndicator()), - ), - child: const MyApp(), - ), - ); -} - -/// The API service for the example -class ApiService { - ApiService(); - - Future init() async { - return Future.delayed(const Duration(seconds: 1), () => this); - } -} - -/// The data source for the example -class AuthDataSource { - final ApiService apiService; - - AuthDataSource({required this.apiService}); - - Future login() => Future.value('Token'); + runApp(const MyApp()); } -/// The repository for the example -final class AuthRepository { - final AuthDataSource dataSource; - - AuthRepository({required this.dataSource}); - - Future login() => dataSource.login(); - - void dispose() { - // stream.close(); - } -} class MyApp extends StatelessWidget { const MyApp({super.key}); @@ -84,15 +17,26 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', - home: InjectionScope( - injection: ModuleInjection( - parent: InjectionScope.of(context), + home: DependencyScope( + dependency: RootInjection(), + placeholder: const ColoredBox( + color: Colors.white, + child: CupertinoActivityIndicator(), ), - child: BlocProvider( - create: (context) => DefaultBloc( - InjectionScope.of(context).authRepository, + builder: (context) => DependencyScope( + dependency: ModuleInjection( + parent: DependencyProvider.of(context), + ), + placeholder: const ColoredBox( + color: Colors.white, + child: CupertinoActivityIndicator(), + ), + builder: (context) => BlocProvider( + create: (context) => DefaultBloc( + DependencyProvider.of(context).authRepository, + ), + child: const MyHomePage(), ), - child: const MyHomePage(), ), ), ); diff --git a/example/lib/src/default_bloc.dart b/example/lib/src/bloc/default_bloc.dart similarity index 91% rename from example/lib/src/default_bloc.dart rename to example/lib/src/bloc/default_bloc.dart index 01a2dfe..e940146 100644 --- a/example/lib/src/default_bloc.dart +++ b/example/lib/src/bloc/default_bloc.dart @@ -1,5 +1,6 @@ import 'package:bloc/bloc.dart'; -import 'package:example/main.dart'; + +import '../services.dart'; part 'default_event.dart'; part 'default_state.dart'; diff --git a/example/lib/src/default_event.dart b/example/lib/src/bloc/default_event.dart similarity index 100% rename from example/lib/src/default_event.dart rename to example/lib/src/bloc/default_event.dart diff --git a/example/lib/src/default_state.dart b/example/lib/src/bloc/default_state.dart similarity index 100% rename from example/lib/src/default_state.dart rename to example/lib/src/bloc/default_state.dart diff --git a/example/lib/src/dependencies.dart b/example/lib/src/dependencies.dart new file mode 100644 index 0000000..76d5d10 --- /dev/null +++ b/example/lib/src/dependencies.dart @@ -0,0 +1,32 @@ + +import 'package:depend/depend.dart'; +import 'package:example/src/services.dart'; + +class RootInjection extends DependencyContainer { + late final ApiService apiService; + + @override + Future init() async { + apiService = await ApiService().init(); + } +} + +class ModuleInjection extends DependencyContainer { + late final AuthRepository authRepository; + + ModuleInjection({required super.parent}); + + @override + Future init() async { + authRepository = AuthRepository( + dataSource: AuthDataSource( + apiService: parent.apiService, + ), + ); + } + + @override + void dispose() { + authRepository.dispose(); + } +} diff --git a/example/lib/src/services.dart b/example/lib/src/services.dart new file mode 100644 index 0000000..ff16acc --- /dev/null +++ b/example/lib/src/services.dart @@ -0,0 +1,38 @@ + +import 'dart:async'; + +/// The API service for the example +class ApiService { + ApiService(); + + Future init() async { + await Future.delayed(const Duration(seconds: 1), () {}); + return this; + } +} + +/// The data source for the example +class AuthDataSource { + final ApiService apiService; + + AuthDataSource({required this.apiService}); + + Future login() => Future.value('Token'); +} + +/// The repository for the example +final class AuthRepository { + final AuthDataSource dataSource; + + final StreamController _stream; + + Stream get stream => _stream.stream; + + AuthRepository({required this.dataSource}): _stream = StreamController.broadcast(); + + Future login() => dataSource.login(); + + void dispose() { + _stream.close(); + } +} \ No newline at end of file diff --git a/example/pubspec.lock b/example/pubspec.lock index 7973585..2c5dc1a 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -63,7 +63,7 @@ packages: path: ".." relative: true source: path - version: "2.0.3" + version: "4.0.0-dev" fake_async: dependency: transitive description: diff --git a/lib/depend.dart b/lib/depend.dart index feacb79..a0b6278 100644 --- a/lib/depend.dart +++ b/lib/depend.dart @@ -1,5 +1,7 @@ library; -export 'src/injection.dart'; +export 'src/context_extension.dart'; +export 'src/dependency_container.dart'; +export 'src/dependency_provider.dart'; +export 'src/dependency_scope.dart'; export 'src/injection_exception.dart'; -export 'src/injection_scope.dart'; diff --git a/lib/src/context_extension.dart b/lib/src/context_extension.dart new file mode 100644 index 0000000..284cc2b --- /dev/null +++ b/lib/src/context_extension.dart @@ -0,0 +1,36 @@ +import 'package:depend/depend.dart'; +import 'package:flutter/widgets.dart'; + +/// An extension on [BuildContext] that provides convenient methods +/// for accessing dependencies registered via [DependencyProvider]. +extension DependencyContext on BuildContext { + /// Retrieves a dependency of type [T] from the nearest [DependencyProvider] + /// in the widget tree. + /// + /// This method requires that a [DependencyProvider] of type [T] is present + /// in the widget tree. If not, it will throw an error. + /// + /// Example: + /// ```dart + /// MyDependencyContainer myDependency = context.depend(); + /// ``` + T depend>() => + DependencyProvider.of(this); + + /// Retrieves a dependency of type [T] from the nearest [DependencyProvider] + /// in the widget tree, or returns `null` if no matching [DependencyProvider] + /// is found. + /// + /// This method is useful if the dependency is optional and you want to avoid + /// throwing an error when it is not present. + /// + /// Example: + /// ```dart + /// MyDependencyContainer? myDependency = context.maybeDepend(); + /// if (myDependency != null) { + /// // Use the dependency + /// } + /// ``` + T? maybeDepend>() => + DependencyProvider.maybeOf(this); +} diff --git a/lib/src/dependency_container.dart b/lib/src/dependency_container.dart new file mode 100644 index 0000000..52d8569 --- /dev/null +++ b/lib/src/dependency_container.dart @@ -0,0 +1,122 @@ +import 'package:depend/depend.dart'; +import 'package:flutter/foundation.dart'; + +/// {@template dependencies_Injection} +/// An abstract class that serves as a foundation for managing dependencies +/// in a hierarchical and structured way. +/// +/// The [DependencyContainer] class provides mechanisms for: +/// - Initializing dependencies via the [init] method. +/// - Accessing a parent dependency container for hierarchical setups. +/// - Managing the lifecycle of dependencies, including cleanup with [dispose]. +/// +/// ### Usage +/// +/// Extend this class to create your own dependency container and implement +/// initialization and cleanup logic: +/// +/// ```dart +/// class MyDependencyContainer extends DependencyContainer { +/// @override +/// Future init() async { +/// // Initialize your dependencies here +/// } +/// +/// @override +/// void dispose() { +/// // Clean up resources +/// myStreamController.close(); +/// super.dispose(); +/// } +/// } +/// ``` +/// {@endtemplate} +abstract class DependencyContainer { + /// Creates an instance of [DependencyContainer]. + /// + /// - The [parent] parameter allows this container to reference another + /// container as its parent, enabling hierarchical dependency setups. + DependencyContainer({T? parent}) : _parent = parent; + + final T? _parent; + + /// Provides access to the parent dependency container. + /// + /// Throws an [InjectionException] if the parent is not set or initialized. + /// + /// ### Example + /// ```dart + /// final parentContainer = parent; + /// ``` + @nonVirtual + T get parent { + if (_parent == null) { + throw InjectionException( + 'Parent in $runtimeType is not initialized', + stackTrace: StackTrace.current, + ); + } + return _parent!; + } + + /// Tracks whether the container's initialization logic has been executed. + /// + /// This ensures that the [init] method is called only once during the + /// container's lifecycle. + bool _isInitialization = false; + + /// Returns `true` if the container has been initialized. + bool get isInitialization => _isInitialization; + + /// The entry point for initializing dependencies within this container. + /// + /// - This method should be overridden by subclasses to provide custom + /// initialization logic. + /// - Ensure you call `super.init()` when overriding to maintain the + /// container's initialization state. + /// + /// ### Example + /// ```dart + /// @override + /// Future init() async { + /// await super.init(); + /// // Custom initialization logic + /// someDependency = await initializeDependency(); + /// } + /// ``` + @mustCallSuper + Future init(); + + /// A wrapper method that ensures [init] is called only once. + /// + /// This method checks if the container is already initialized and skips + /// the initialization if it is. + /// + /// ### Example + /// ```dart + /// await dependencyContainer.inject(); + /// ``` + Future inject() async { + if (_isInitialization) return; + _isInitialization = true; + await init(); + } + + /// Cleans up resources or dependencies managed by this container. + /// + /// Override this method to perform any necessary cleanup, such as closing + /// streams, canceling timers, or disposing objects. + /// + /// The base implementation does nothing, so overriding this method is + /// optional unless cleanup is required. + /// + /// ### Example + /// ```dart + /// @override + /// void dispose() { + /// myStreamController.close(); + /// super.dispose(); + /// } + /// ``` + void dispose() {} +} diff --git a/lib/src/dependency_provider.dart b/lib/src/dependency_provider.dart new file mode 100644 index 0000000..20af52e --- /dev/null +++ b/lib/src/dependency_provider.dart @@ -0,0 +1,112 @@ +import 'package:depend/depend.dart'; +import 'package:flutter/widgets.dart'; + +/// A widget that provides a [DependencyContainer] (or its subclass) to its subtree. +/// +/// [DependencyProvider] acts as a bridge to pass dependencies down the widget +/// tree. It allows widgets to access the provided dependency using the static +/// [of] or [maybeOf] methods. +/// +/// ### Usage +/// +/// ```dart +/// DependencyProvider( +/// injection: MyDependency(), +/// child: MyApp(), +/// ); +/// ``` +/// +/// Widgets in the subtree can access the dependency as follows: +/// +/// ```dart +/// final myDependency = DependencyProvider.of(context); +/// ``` +class DependencyProvider> + extends InheritedWidget { + /// Creates a [DependencyProvider] widget. + /// + /// - The [dependency] parameter is the dependency instance to provide to + /// the widget tree. It must not be `null`. + /// - Either [builder] or [child] must be provided: + /// - [builder]: A function that returns the child widget. This is useful + /// when the child requires access to the dependency during its creation. + /// - [child]: The widget below this widget in the tree. + DependencyProvider({ + required this.dependency, + super.key, + Widget Function()? builder, + Widget? child, + }) : super(child: child ?? builder?.call() ?? const Offstage()); + + /// The dependency instance being provided to the subtree. + final T dependency; + + /// Provides the nearest [DependencyContainer] of type [T] from the widget tree. + /// + /// This method returns `null` if no [DependencyProvider] of the specified + /// type is found. + /// + /// - The [listen] parameter determines whether the widget should rebuild + /// when the [DependencyProvider] updates: + /// - If `true`, the context will subscribe to changes and rebuild when + /// the dependency updates. + /// - If `false`, the context will not rebuild when the dependency changes. + /// + /// ### Example + /// + /// ```dart + /// final myDependency = DependencyProvider.maybeOf(context); + /// if (myDependency != null) { + /// // Use the dependency + /// } + /// ``` + static T? maybeOf>( + BuildContext context, { + bool listen = false, + }) => + listen + ? context + .dependOnInheritedWidgetOfExactType>() + ?.dependency + : context + .getInheritedWidgetOfExactType>() + ?.dependency; + + /// Provides the nearest [DependencyContainer] of type [T] from the widget tree. + /// + /// This method throws an [ArgumentError] if no [DependencyProvider] of the + /// specified type is found. + /// + /// - The [listen] parameter determines whether the widget should rebuild + /// when the [DependencyProvider] updates: + /// - If `true`, the context will subscribe to changes and rebuild when + /// the dependency updates. + /// - If `false`, the context will not rebuild when the dependency changes. + /// + /// ### Example + /// + /// ```dart + /// final myDependency = DependencyProvider.of(context); + /// // Use the dependency + /// ``` + static T of>( + BuildContext context, { + bool listen = false, + }) => + maybeOf(context, listen: listen) ?? _notFound(); + + /// Helper method to throw an error when a dependency of type [T] is not found. + static Never _notFound>() => + throw ArgumentError( + 'DependencyProvider.of<$T>() called with a context that does not contain an $T.'); + + /// Determines whether widgets that depend on this [DependencyProvider] should rebuild. + /// + /// Returns `true` if the provided [dependency] has changed since the last + /// build, and `false` otherwise. + /// + /// This is used by the Flutter framework to optimize widget rebuilding. + @override + bool updateShouldNotify(DependencyProvider oldWidget) => + dependency != oldWidget.dependency; +} diff --git a/lib/src/dependency_scope.dart b/lib/src/dependency_scope.dart new file mode 100644 index 0000000..c6ea276 --- /dev/null +++ b/lib/src/dependency_scope.dart @@ -0,0 +1,123 @@ +import 'dart:async'; + +import 'package:depend/depend.dart'; +import 'package:flutter/widgets.dart'; + +/// A widget that initializes and provides a [DependencyContainer] to its subtree. +/// +/// [DependencyScope] manages the lifecycle of the dependency by: +/// - Initializing it using the `inject` method. +/// - Disposing of it when it is no longer needed or replaced. +/// +/// This widget is useful for scoping dependencies to a specific portion of the widget tree. +/// +/// ### Example +/// +/// ```dart +/// DependencyScope( +/// dependency: MyDependency(), +/// placeholder: CircularProgressIndicator(), +/// errorBuilder: (error) => Text('Error: $error'), +/// builder: (context) { +/// final dependency = DependencyProvider.of(context); +/// return MyWidget(dependency: dependency); +/// }, +/// ); +/// ``` +class DependencyScope> + extends StatefulWidget { + /// Creates a [DependencyScope] widget. + /// + /// - The [dependency] parameter specifies the [DependencyContainer] instance + /// to be managed and provided to the subtree. + /// - The [builder] parameter is a function that builds the widget tree once + /// the dependency has been initialized. + /// - The optional [placeholder] is displayed while the dependency is being + /// initialized. + /// - The optional [errorBuilder] is called if an error occurs during + /// initialization. + const DependencyScope({ + required this.dependency, + required this.builder, + this.placeholder, + this.errorBuilder, + super.key, + }); + + /// The dependency to be managed and provided. + final T dependency; + + /// A builder function that constructs the widget tree once the dependency + /// has been initialized. + final Widget Function(BuildContext context) builder; + + /// A widget to display while the dependency is being initialized. + final Widget? placeholder; + + /// A builder function to construct a widget if an error occurs during + /// dependency initialization. + final Widget Function(Object? error)? errorBuilder; + + @override + State> createState() => _DependencyScopeState(); +} + +class _DependencyScopeState> + extends State> { + late Future _initFuture; + + @override + void initState() { + super.initState(); + _initFuture = _initializeInit(); + } + + @override + void didUpdateWidget(covariant DependencyScope oldWidget) { + super.didUpdateWidget(oldWidget); + + // Dispose of the old dependency and initialize the new one if it has changed. + if (widget.dependency != oldWidget.dependency) { + oldWidget.dependency.dispose(); + _initFuture = _initializeInit(); + } + } + + @override + void dispose() { + // Dispose of the managed dependency when the widget is removed from the tree. + widget.dependency.dispose(); + super.dispose(); + } + + /// Initializes the dependency using its [inject] method. + Future _initializeInit() => widget.dependency.inject(); + + @override + Widget build(BuildContext context) => FutureBuilder( + future: _initFuture, + builder: (context, snapshot) { + if (snapshot.hasError) { + // Display the error widget if an error occurs during initialization. + return widget.errorBuilder?.call(snapshot.error) ?? + ErrorWidget(snapshot.error!); + } + + return switch (snapshot.connectionState) { + // Show the placeholder while waiting for the dependency to initialize. + ConnectionState.none || + ConnectionState.waiting || + ConnectionState.active => + widget.placeholder ?? const SizedBox.shrink(), + + // Provide the dependency once initialization is complete. + ConnectionState.done => DependencyProvider( + dependency: widget.dependency, + child: Builder( + builder: widget.builder, + ), + ), + }; + }, + ); +} diff --git a/lib/src/injection.dart b/lib/src/injection.dart deleted file mode 100644 index ed6db37..0000000 --- a/lib/src/injection.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:depend/depend.dart'; -import 'package:flutter/foundation.dart'; - -/// {@template dependencies_Injection} -/// An abstract class that serves as a base for managing dependencies in your application. -/// It provides a structure for initializing dependencies and accessing a parent Injection if needed. -/// -/// The `Injection` class is designed to be extended by your own dependency Injection classes, -/// allowing you to define initialization logic and manage dependencies effectively. -/// -/// ### Example -/// -/// ```dart -/// class MyInjectionScope extends Injection { -/// @override -/// Future init() async { -/// // Initialize your dependencies here -/// await log(() async { -/// // Initialize a specific dependency -/// return await initializeMyDependency(); -/// }); -/// } -/// } -/// ``` -/// {@endtemplate} -abstract class Injection { - /// Creates a new instance of [Injection]. - /// - /// The optional [parent] parameter allows you to reference a parent dependencies Injection, - /// enabling hierarchical dependency management. - Injection({T? parent}) : _parent = parent; - - final T? _parent; - - /// Provides access to the parent dependencies Injection. - /// - /// Throws an [Exception] if the parent is not initialized. - /// - /// ### Example - /// - /// ```dart - /// final parentLibrary = parent; - /// ``` - @nonVirtual - T get parent { - if (_parent == null) { - throw InjectionException('Parent in $runtimeType is not initialized'); - } - return _parent!; - } - - /// Initializes the dependencies. - /// - /// This method should be overridden in subclasses to implement the initialization logic - /// for your dependencies. The `@mustCallSuper` annotation indicates that if you override - /// this method, you should call `super.init()` to ensure proper initialization. - /// - /// ### Example - /// - /// ```dart - /// @override - /// Future init() async { - /// await super.init(); - /// // Your initialization code here - /// } - /// ``` - @mustCallSuper - Future init(); - - /// Cleans up resources used by the dependencies. - /// - /// This method can be overridden to release any resources, close streams, or dispose - /// of objects that were initialized in the `init` method or elsewhere in the Injection. - /// - /// The base implementation is empty, so calling `super.dispose()` is optional unless - /// overridden by subclasses to include specific cleanup logic. - /// - /// ### Example - /// - /// ```dart - /// @override - /// void dispose() { - /// // Close a stream controller - /// myStreamController.close(); - /// - /// // Dispose of other resources - /// someDependency.dispose(); - /// - /// super.dispose(); - /// } - /// ``` - void dispose() {} - - /// Logs the initialization process of a dependency. - /// - /// This method executes the provided [callback] and logs the time taken to complete it. - /// In release mode, the [callback] is executed without logging to avoid performance overhead. - /// In debug mode, it measures the execution time and prints it using `debugPrint`. - /// - /// ### Example - /// - /// ```dart - /// await log(() async { - /// return await initializeMyDependency(); - /// }); - /// ``` -} diff --git a/lib/src/injection_exception.dart b/lib/src/injection_exception.dart index ba3fdab..9f2159c 100644 --- a/lib/src/injection_exception.dart +++ b/lib/src/injection_exception.dart @@ -6,20 +6,24 @@ /// message to be passed that describes the cause of the error when creating /// an instance of the exception. /// -/// It's used for scenarios where dependency injection fails or encounters -/// an error during execution. +/// It also stores the [stackTrace], which provides additional context +/// about where the exception occurred. /// /// Example: /// ```dart -/// throw InjectionException('Error initializing dependency'); +/// try { +/// throw InjectionException('Error initializing dependency'); +/// } catch (e, stack) { +/// throw InjectionException('Error initializing dependency', stackTrace: stack); +/// } /// ``` /// {@endtemplate} class InjectionException implements Exception { /// {@macro injection_exception} /// /// Constructor that takes a [message] — a string message about the error, - /// which will be stored in the exception. - InjectionException(this.message); + /// and an optional [stackTrace], which provides details of where the error occurred. + InjectionException(this.message, {this.stackTrace}); /// {@template message} /// The error [message] related to dependency injection. @@ -28,4 +32,15 @@ class InjectionException implements Exception { /// for logging or displaying to the user. /// {@endtemplate} final String message; + + /// {@template stackTrace} + /// The [stackTrace] that provides context about where the exception occurred. + /// + /// This is particularly useful for debugging and logging purposes. + /// {@endtemplate} + final StackTrace? stackTrace; + + @override + String toString() => + 'InjectionException: $message${stackTrace != null ? '\n$stackTrace' : ''}'; } diff --git a/lib/src/injection_scope.dart b/lib/src/injection_scope.dart deleted file mode 100644 index 068d83b..0000000 --- a/lib/src/injection_scope.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'dart:async'; - -import 'package:depend/depend.dart'; -import 'package:flutter/widgets.dart'; - -/// {@template dependencies_class} -/// An `InheritedWidget` that provides access to a [Injection] -/// instance throughout the widget tree. -/// -/// The `InjectionScope` widget initializes the provided [injection] and ensures -/// it's available to all descendant widgets. It handles asynchronous -/// initialization and can display a [placeholder] widget while the [injection] -/// is being initialized. -/// -/// ### Example -/// -/// ```dart -/// class MyInjection extends Injection { -/// @override -/// Future init() async { -/// // Initialize your dependencies here -/// } -/// } -/// -/// void main() { -/// runApp( -/// InjectionScope( -/// injection: MyInjection(), -/// placeholder: CircularProgressIndicator(), -/// child: MyApp(), -/// ), -/// ); -/// } -/// ``` -/// {@endtemplate} -class InjectionScope> extends InheritedWidget { - /// {@macro dependencies_class} - /// - /// Creates a [InjectionScope] widget. - /// - /// The [injection] parameter must not be null and is the [Injection] - /// instance that will be provided to descendants. - /// - /// The [child] parameter is the widget below this widget in the tree. - /// - /// The [placeholder] widget is displayed while the [injection] is initializing. - /// If [placeholder] is not provided, [child] is displayed immediately. - InjectionScope({ - required this.injection, - required super.child, - super.key, - this.placeholder, - }) { - injection.init().then((val) { - completer.complete(injection); - }).catchError((Object error) { - completer.completeError(error, StackTrace.current); - }); - } - - /// A [Completer] to handle the asynchronous initialization of the [injection]. - final Completer completer = Completer(); - - /// The instance of [Injection] to provide to the widget tree. - final T injection; - - /// An optional widget to display while the [injection] is initializing. - final Widget? placeholder; - - /// {@template dependencies_maybe_of} - /// Provides the nearest [Injection] of type [T] up the widget tree. - /// - /// Returns `null` if no such [InjectionScope] is found. - /// - /// The [listen] parameter determines whether the context will rebuild when - /// the [InjectionScope] updates. If [listen] is `true`, the context will - /// subscribe to changes; otherwise, it will not. - /// - /// ### Example - /// - /// ```dart - /// final injection = InjectionScope.maybeOf(context); - /// if (injection != null) { - /// // Use the injection - /// } - /// ``` - /// {@endtemplate} - static T? maybeOf>( - BuildContext context, { - bool listen = false, - }) => - listen - ? context - .dependOnInheritedWidgetOfExactType>() - ?.injection - : context - .getInheritedWidgetOfExactType>() - ?.injection; - - /// {@template dependencies_of} - /// Provides the nearest [Injection] of type [T] up the widget tree. - /// - /// Throws an [ArgumentError] if no such [InjectionScope] is found. - /// - /// The [listen] parameter determines whether the context will rebuild when - /// the [InjectionScope] updates. If [listen] is `true`, the context will - /// subscribe to changes; otherwise, it will not. - /// - /// ### Example - /// - /// ```dart - /// final injection = InjectionScope.of(context); - /// // Use the injection - /// ``` - /// {@endtemplate} - static T of>( - BuildContext context, { - bool listen = false, - }) => - maybeOf(context, listen: listen) ?? _notFound(); - - static Never _notFound>() => throw ArgumentError( - 'InjectionScope.of<$T>() called with a context that does not contain an $T.'); - - @override - Widget get child => FutureBuilder( - future: completer.future, - builder: (context, snapshot) { - if (snapshot.hasError) { - return ErrorWidget(snapshot.error!); - } - return switch (snapshot.connectionState) { - ConnectionState.none || - ConnectionState.waiting || - ConnectionState.active => - placeholder ?? super.child, - ConnectionState.done => _DisposeDependency( - injection: injection, - child: super.child, - ) - }; - }, - ); - - @override - bool updateShouldNotify(InjectionScope oldWidget) => - injection != oldWidget.injection; -} - -class _DisposeDependency> extends StatefulWidget { - const _DisposeDependency({ - required this.child, - required this.injection, - super.key, - }); - - final T injection; - final Widget child; - - @override - State<_DisposeDependency> createState() => _DisposeDependencyState(); -} - -class _DisposeDependencyState> - extends State<_DisposeDependency> { - @override - void dispose() { - widget.injection.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) => widget.child; -} diff --git a/pubspec.yaml b/pubspec.yaml index ddec0cc..f4873c4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: depend description: "depend simplifies dependency management in Flutter apps, providing easy initialization and access to services across the widget tree." -version: 3.0.0 +version: 4.0.0 homepage: https://www.contributors.info/repository/depend @@ -31,7 +31,9 @@ dependencies: flutter: sdk: flutter + dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^3.0.0 + mocktail: ^1.0.4 diff --git a/test/dependency_container_test.dart b/test/dependency_container_test.dart new file mode 100644 index 0000000..be99d86 --- /dev/null +++ b/test/dependency_container_test.dart @@ -0,0 +1,84 @@ +import 'package:depend/depend.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Тестовая реализация DependencyContainer +class TestDependencyContainer + extends DependencyContainer { + TestDependencyContainer({super.parent}); + bool disposed = false; + + @override + void dispose() { + disposed = true; + super.dispose(); + } + + @override + Future init() async {} +} + +void main() { + group('DependencyContainer', () { + test('should provide access to parent if initialized', () { + final parentContainer = TestDependencyContainer(); + final childContainer = TestDependencyContainer(parent: parentContainer); + + expect(childContainer.parent, equals(parentContainer)); + }); + + test('should throw InjectionException if parent is not initialized', () { + final container = TestDependencyContainer(); + + expect(() => container.parent, throwsA(isA())); + }); + + test('should call init and set initialized to true', () async { + final container = TestDependencyContainer(); + + expect(container.isInitialization, isFalse); + + await container.inject(); + + expect(container.isInitialization, isTrue); + }); + + test('should call dispose and set disposed to true', () { + final container = TestDependencyContainer(); + + expect(container.disposed, isFalse); + + container.dispose(); + + expect(container.disposed, isTrue); + }); + + test('take parent then parent null', () { + final container = TestDependencyContainer(); + + expect(() => container.parent, throwsA(isA())); + try { + container.parent; + } catch (err) { + expect(err.toString(), isA()); + } + + container.dispose(); + }); + + test('isInitialization', () async { + final container = TestDependencyContainer(); + + expect(container.isInitialization, isFalse); + + await container.inject(); + + expect(container.isInitialization, isTrue); + + await container.inject(); + + expect(container.isInitialization, isTrue); + + container.dispose(); + }); + }); +} diff --git a/test/dependency_library_test.dart b/test/dependency_library_test.dart deleted file mode 100644 index 6ecba7a..0000000 --- a/test/dependency_library_test.dart +++ /dev/null @@ -1,212 +0,0 @@ -import 'package:depend/depend.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; - -class TestInjectionScopeLibrary extends Injection { - @override - Future init() async { - // Имитируем инициализацию - await Future.delayed(const Duration(milliseconds: 100), () {}); - } -} - -class FaultyInjectionScopeLibrary - extends Injection { - FaultyInjectionScopeLibrary(); - - @override - Future init() async { - await Future.delayed(const Duration(milliseconds: 100), () {}); - - // Выбрасываем исключение - throw Exception('Initialization failed'); - } -} - -class MockLibrary extends Injection { - MockLibrary() : super(); - - @override - Future init() async {} -} - -void main() { - group('InjectionScope Widget', () { - testWidgets('maybeOf should find the library in the widget tree', - (tester) async { - // Создаем виджет, в котором обернут наш Dependencies. - await tester.pumpWidget( - InjectionScope( - injection: MockLibrary(), - child: Builder( - builder: (context) { - // Вызываем maybeOf внутри контекста, чтобы проверить извлечение. - final library = - InjectionScope.maybeOf(context, listen: true); - expect(library, isNotNull); - return Container(); - }, - ), - ), - ); - }); - - test('updateShouldNotify returns true when libraries are different', () { - final oldLibrary = MockLibrary(); - final newLibrary = MockLibrary(); - - final oldDependencies = InjectionScope( - injection: oldLibrary, - child: const SizedBox(), - ); - - final newDependencies = InjectionScope( - injection: newLibrary, - child: const SizedBox(), - ); - - // Проверяем, что updateShouldNotify вернет true, если библиотеки разные - expect(newDependencies.updateShouldNotify(oldDependencies), isTrue); - }); - - test('updateShouldNotify returns false when libraries are the same', () { - final library = MockLibrary(); - - final oldDependencies = InjectionScope( - injection: library, - child: const SizedBox(), - ); - - final newDependencies = InjectionScope( - injection: library, - child: const SizedBox(), - ); - - // Проверяем, что updateShouldNotify вернет false, если библиотеки одинаковые - expect(newDependencies.updateShouldNotify(oldDependencies), isFalse); - }); - - testWidgets('maybeOf should return null when library is not found', - (tester) async { - // Проверяем, что if no Dependencies widget is found, maybeOf returns null. - await tester.pumpWidget( - Builder( - builder: (context) { - final library = - InjectionScope.maybeOf(context, listen: true); - expect(library, isNull); - return Container(); - }, - ), - ); - }); - - testWidgets('Предоставляет library потомкам', (tester) async { - final library = TestInjectionScopeLibrary(); - - await tester.pumpWidget( - InjectionScope( - injection: library, - child: Builder( - builder: (context) { - final retrievedLibrary = - InjectionScope.of(context); - expect(retrievedLibrary, equals(library)); - return Container(); - }, - ), - ), - const Duration(milliseconds: 200), - ); - }); - - testWidgets('Показывает placeholder во время инициализации', - (tester) async { - final library = TestInjectionScopeLibrary(); - const placeholderKey = Key('placeholder'); - - await tester.pumpWidget( - InjectionScope( - injection: library, - placeholder: Container(key: placeholderKey), - child: Container(), - ), - ); - - expect(find.byKey(placeholderKey), findsOneWidget); - - // Ждем завершения инициализации - await tester.pumpAndSettle(); - - expect(find.byKey(placeholderKey), findsNothing); - }); - - testWidgets('Child отображается после инициализации', (tester) async { - final library = TestInjectionScopeLibrary(); - const childKey = Key('child'); - - await tester.pumpWidget( - InjectionScope( - injection: library, - child: const SizedBox(key: childKey), - ), - ); - - expect(find.byKey(childKey), findsOneWidget); - - await tester.pumpAndSettle(); - - expect(find.byKey(childKey), findsOneWidget); - }); - - testWidgets('maybeOf возвращает null, если не найдено', (tester) async { - await tester.pumpWidget( - Builder( - builder: (context) { - final library = - InjectionScope.maybeOf(context); - expect(library, isNull); - return Container(); - }, - ), - ); - }); - - testWidgets('of бросает исключение, если зависимость не найдена', - (tester) async { - await tester.pumpWidget( - Builder( - builder: (context) { - expect(() => InjectionScope.of(context), - throwsArgumentError); - return Container(); - }, - ), - ); - }); - - testWidgets('Обрабатывает ошибку инициализации в методе init', - (tester) async { - final library = FaultyInjectionScopeLibrary(); - - await tester.pumpWidget( - InjectionScope( - injection: library, - placeholder: Container(key: const Key('placeholder')), - child: - Builder(builder: (context) => Container(key: const Key('child'))), - ), - ); - - expect(find.byKey(const Key('placeholder')), findsOneWidget); - expect(find.byKey(const Key('child')), findsNothing); - expect(find.byType(ErrorWidget), findsNothing); - - await tester.pumpAndSettle(); - - expect(find.byKey(const Key('placeholder')), findsNothing); - expect(find.byKey(const Key('child')), findsNothing); - expect(find.byType(ErrorWidget), findsOneWidget); - }); - }); -} diff --git a/test/dependency_provider_test.dart b/test/dependency_provider_test.dart index 1d6123b..2687669 100644 --- a/test/dependency_provider_test.dart +++ b/test/dependency_provider_test.dart @@ -1,39 +1,152 @@ -import 'package:depend/depend.dart'; // Замените на фактический путь +import 'package:depend/depend.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; -// Моковый класс для тестирования -class MockInjectionScopeLibrary extends Injection { - MockInjectionScopeLibrary({MockInjectionScopeLibrary? parent}) - : super(parent: parent); - bool initCalled = false; - - @override - Future init() async { - initCalled = true; - // Имитируем инициализацию - await Future.delayed(const Duration(milliseconds: 100), () => 1); - } +// Мок зависимости +class MockDependencyContainer extends Mock + implements DependencyContainer { + late String value; } void main() { - group('InjectionScopeLibrary', () { - test('Должен вернуть родителя, когда он инициализирован', () async { - final parent = MockInjectionScopeLibrary(); - final child = MockInjectionScopeLibrary(parent: parent); - expect(child.parent, equals(parent)); + group('DependencyScope Tests', () { + testWidgets('retrieves dependency correctly', (tester) async { + // Создание контейнера с зависимостью + final mockDependency = MockDependencyContainer(); + final k1 = GlobalKey(); + final mkey = GlobalKey(); + + when(mockDependency.init).thenAnswer((_) async { + mockDependency.value = 'Test Dependency'; + }); + + await mockDependency.init(); + + // Строим виджет с DependencyScope + await tester.pumpWidget( + MaterialApp( + key: mkey, + home: DependencyProvider( + dependency: mockDependency, + child: Builder( + builder: (context) { + final dependency = + DependencyProvider.of(context); + return Text(key: k1, dependency.value); + }, + )), + ), + ); + + await tester.pumpAndSettle(); + + // Проверяем, что текст "Test Dependency" отображается + expect(find.text('Test Dependency'), findsOneWidget); + expect( + k1.currentContext!.depend(), mockDependency); + expect( + mkey.currentContext?.maybeDepend(), isNull); + }); + + testWidgets('throws error when dependency is not found', (tester) async { + // Строим виджет без DependencyScope + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + try { + DependencyProvider.of(context); + return const Text('No error'); + } catch (error) { + return ErrorWidget(error); + } + }, + ), + ), + ); + + await tester.pumpAndSettle(); + + // Проверяем, что выброшена ошибка + expect(find.byType(ErrorWidget), findsOneWidget); }); - test('Должен бросить исключение, когда родитель равен null', () { - final library = MockInjectionScopeLibrary(); + testWidgets('maybeOf returns null when dependency is not found', + (tester) async { + // Строим виджет без DependencyScope + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + // Пытаемся получить зависимость через maybeOf, и она должна вернуть null + final dependency = + DependencyProvider.maybeOf(context); + return Text(dependency?.value ?? 'No dependency'); + }, + ), + ), + ); - expect(() => library.parent, throwsException); + // Проверяем, что в тексте будет "No dependency" + expect(find.text('No dependency'), findsOneWidget); }); - test('Метод init должен быть вызван', () async { - final library = MockInjectionScopeLibrary(); - await library.init(); + testWidgets('updateShouldNotify returns true if dependency changes', + (tester) async { + // Строим виджет с DependencyScope + final mockDependency1 = MockDependencyContainer(); + final mockDependency2 = MockDependencyContainer(); + + when(mockDependency1.init).thenAnswer((_) async { + mockDependency1.value = 'Dependency 1'; + }); + when(mockDependency2.init).thenAnswer((_) async { + mockDependency2.value = 'Dependency 2'; + }); + + await mockDependency1.init(); + await mockDependency2.init(); + + var a = true; + + // Создаём StatefulWidget для теста обновления + await tester.pumpWidget( + MaterialApp( + home: StatefulBuilder( + builder: (context, setState) => + DependencyProvider( + dependency: a ? mockDependency1 : mockDependency2, + child: Builder( + builder: (context) { + final dependency = + DependencyProvider.of( + context, + listen: true); + return GestureDetector( + onTap: () { + setState(() { + a = !a; + }); + }, + child: Text(dependency.value), // Текущая зависимость + ); + }, + )), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Проверяем начальное значение + expect(find.text('Dependency 1'), findsOneWidget); + + await tester.tap(find.text('Dependency 1')); + await tester.pumpAndSettle(); - expect(library.initCalled, isTrue); + // Проверяем, что зависимость изменилась + expect(find.text('Dependency 2'), findsOneWidget); }); }); } diff --git a/test/dependency_scope_test.dart b/test/dependency_scope_test.dart new file mode 100644 index 0000000..c5343a4 --- /dev/null +++ b/test/dependency_scope_test.dart @@ -0,0 +1,148 @@ +import 'package:depend/depend.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +// Фейковая реализация DependencyContainer +class MockDependencyContainer extends Mock + implements DependencyContainer {} + +class MockExceptionDependencyContainer extends Mock + implements DependencyContainer {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('DependencyScope Tests', () { + testWidgets('renders placeholder when initializing', (tester) async { + final mockDependency = MockDependencyContainer(); + + when(mockDependency.init).thenAnswer((_) async {}); + when(mockDependency.inject).thenAnswer((_) async {}); + + // Строим виджет с placeholder + await tester.pumpWidget( + DependencyScope( + dependency: mockDependency, + builder: (context) => Container(), + placeholder: const CircularProgressIndicator(), + ), + ); + + // Ожидаем, что будет отображаться placeholder + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + // Завершаем инициализацию + await tester.pumpAndSettle(); + + // Ожидаем, что виджет будет обновлен, и placeholder исчезнет + expect(find.byType(CircularProgressIndicator), findsNothing); + }); + + testWidgets('renders errorBuilder when init fails', (tester) async { + final mockDependency = MockExceptionDependencyContainer(); + + when(mockDependency.inject).thenAnswer((_) async { + throw Exception(); + }); + + // Строим виджет с errorBuilder + await tester.pumpWidget( + MaterialApp( + home: DependencyScope( + dependency: mockDependency, + builder: (context) => Container(), + errorBuilder: (context) => + const Text('Error: Initialization error'), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Ожидаем, что будет отображено сообщение об ошибке + await expectLater( + find.text('Error: Initialization error'), findsOneWidget); + }); + + testWidgets('renders errorBuilder when init fails', (tester) async { + final mockDependency = MockDependencyContainer(); + + when(mockDependency.inject).thenAnswer((_) async { + throw Exception(); + }); + + // Строим виджет с errorBuilder + await tester.pumpWidget( + MaterialApp( + home: DependencyScope( + dependency: mockDependency, + builder: (context) => Container(), + ), + ), + ); + await tester.pumpAndSettle(); + + // Ожидаем, что будет отображено сообщение об ошибке + await expectLater(find.byType(ErrorWidget), findsOneWidget); + }); + + testWidgets('calls dispose when the widget is disposed', (tester) async { + final mockDependency = MockDependencyContainer(); + + when(mockDependency.init).thenAnswer((_) async {}); + when(mockDependency.inject).thenAnswer((_) async {}); + + when(mockDependency.dispose).thenAnswer((_) async {}); + + // Строим виджет + await tester.pumpWidget( + DependencyScope( + dependency: mockDependency, + builder: (context) => Container(), + ), + ); + + // Ожидаем инициализацию + await tester.pumpAndSettle(); + + verifyNever(mockDependency.dispose); + + await tester.pumpWidget(Container()); + + // Проверяем, что dispose будет вызван + verify(mockDependency.dispose).called(1); + }); + + testWidgets('re-initializes when injection changes', (tester) async { + final mockDependency1 = MockDependencyContainer(); + final mockDependency2 = MockDependencyContainer(); + + when(mockDependency1.inject).thenAnswer((_) async {}); + when(mockDependency2.inject).thenAnswer((_) async {}); + + // Строим первый виджет + await tester.pumpWidget( + DependencyScope( + dependency: mockDependency1, + builder: (context) => Container(), + ), + ); + + // Завершаем первую инициализацию + await tester.pumpAndSettle(); + + // Строим новый виджет с другой зависимостью + await tester.pumpWidget( + DependencyScope( + dependency: mockDependency2, + builder: (context) => Container(), + ), + ); + + await tester.pumpAndSettle(); + + // Проверяем, что инициализация новой зависимости произошла + }); + }); +} diff --git a/test/test.dart b/test/test.dart index 1186ae2..a152bc5 100644 --- a/test/test.dart +++ b/test/test.dart @@ -1,7 +1,9 @@ -import 'dependency_library_test.dart' as dependency_library_test; +import 'dependency_container_test.dart' as dependency_container_test; import 'dependency_provider_test.dart' as dependency_provider_test; +import 'dependency_scope_test.dart' as dependency_scope_test; void main() { - dependency_library_test.main(); dependency_provider_test.main(); + dependency_scope_test.main(); + dependency_container_test.main(); }