Flutter: Modularized Dependency Injection

Neo Infoway - WEB & Mobile Development Company | Festival | Neo | Infoway | Leading software Development company | Top Software development company in India
Document

Flutter: Modularized Dependency Injection

Let’s say that you’re in the phase where you’re Maintainability of of your Flutter project is a crucial aspect to consider, so it is important to make sure your project is adhering for the best practices to ensure a suitable structure and code quality and maintain it at a level that is satisfactory.In that situation, Separation of Concerns , Encapsulation , coupling , and cohesion These are the aspects you’d like to control in the architecture you choose to build.

It’s better to choose physical separation instead of logical, i.e. break your project up into Dart/Flutter packages instead of simply grouping things into various directories. If you have a tiny Flutter app that is merely the logical separation, you’ll fail to transform the app into physical files that reflect the directory structure you have. This is often due to the fact that it’s easy to violate or ignore architectural restrictions even when there’s no physical separation.

Dependency Injection in modules? How?

It’s obvious that the precise architecture will depend on the project, team and knowledge. I’m not planning to talk about architecture within the context of this article, but instead focus on the way you can arrange Dependency Injection (DI) in the form of a modularized Flutter application.

Refactoring The CounterApp

It’s obvious that the precise architecture will depend on the project, team and knowledge. I’m not planning to talk about architecture within the context of this article, but instead focus on the way you can arrange Dependency Injection (DI) in the form of a modularized Flutter application.

Cross-Cutting Concerns

A cross-cutting concern package generally contains items that impact the entire program and is able to be utilized by all layers. I added DI abstractions in it.

The first is known as DI This interface is accountable for retrieving objects from the DI container.

                    
                        abstract interface class DI {
                            T call({String? instanceName});
                            T get({String? instanceName});
                            T getWithParam(
                            P param, {
                            String? instanceName,
                           });
                           }
                         
                    

The other interface is called DIRegistrar and offers the API for registering dependencies within DI Containers. DI Container. This interface should be accessible only to the implementation and not to its abstraction as well as to the ModuleDependencies abstraction and implementation.

                    
                        typedef FactoryFunc = T Function();
                        typedef FactoryWithParamFunc = T Function(P param);
                        typedef DisposingFunc = FutureOr Function(T instance);
                        abstract interface class DIRegistrar implements DI {
                        void registerFactory(
                        FactoryFunc factoryFunc, {
                        String? instanceName,
                        });
                         void registerFactoryWithParam(
                        FactoryWithParamFunc factoryFunc, {
                         String? instanceName,
                        });
                        void registerSingleton(
                         T instance, {
                        String? instanceName,
                        DisposingFunc? disposingFunc,
                         });
                        void registerLazySingleton(
                        FactoryFunc factoryFunc, {
                        String? instanceName,
                        DisposingFunc? disposingFunc,
                         });
                        }
                        
                    

The third part of the puzzle is ModuleDependencies. It must be implemented by all the modules that have dependencies.

                        
                            abstract class ModuleDependencies {
                            Future register(DIRegistrar do);
                            Future runPostRegistrationActions(DIRegistrar do) => Future.value();
                            }
                            
                        

DI Abstractions Usage

The modules are designed to contain the implementation details and provide only the information essential.

For instance , the reason I included Flutter bloc state management into the presentation package is a design feature, which means it can be changed within this package without needing to alter any other packages in any way.

                    
                        class PresentationModuleDependencies extends ModuleDependencies {
                        @override
                        Future register(DIRegistrar do) async {
                        di.registerFactory(
                        () => CounterCubit(di()),
                         );
                        }
                        }
                         
                    

With the data package, I chose to implement shared_preferences to maintain the status of the counter in between app starts. It is also “known” only to the data package.

                        
                            class DataModuleDependencies extends ModuleDependencies { 
                            @override
                            Future register(DIRegistrar do) async {
                            final sharedPreferences = await SharedPreferences.getInstance();
                            di.registerFactory(
                            () => SharedPreferencesCounterRepository(sharedPreferences),
                            );
                            }
                            }
                            
                        

The application will,naturally, be able to have transitive dependency upon shared_preferences and Flutter_bloc in the end since this is a given and is designed to be part of the base package that eventually combined everything into one artifact. e.g. ipa, apk.

In the initialization phase of our application look through all the installed modules and instruct that they should register their dependencies.

                            
                                Future main() async {
                                WidgetsFlutterBinding.ensureInitialized();
                                final de = GetIrDI();
                                final modules = [
                                DomainModuleDependencies(),
                                PresentationModuleDependencies(),
                                DataModuleDependencies(),
                                ];
                                for (final module in modules) {
                                await module.register(di);
                                }
                                for (final module in modules) {
                                await module.runPostRegistrationActions(di);
                                }
                                runApp(App(di: di));
                                }
                                
                            

You might have been able to see the “GetItDI” program in the line of code below. The implementation is of DIRegistrar which I added to the application package. This implementation is based on get_it, which is the receive_it package. If you decide to change to a different DI Container it is possible to do it as simply as changing the design for DIRegistrar within the application layer, without impacting different packages.

The concept should be evident to you now however, you’re encouraged to look around this repository for the remaining pieces!

Frequently Asked Questions (FAQs)

Dependency injection is a design pattern used to manage the dependencies of objects within an application. In Flutter app development, DI helps decouple components, improve code maintainability, and facilitate testing by allowing dependencies to be provided externally rather than being hardcoded within classes.
Modularized dependency injection in Flutter involves organizing the application into separate modules or features, each with its own set of dependencies and services. This approach allows for better separation of concerns, easier code organization, and more flexible dependency management.
Some benefits of using modularized dependency injection in Flutter apps include:
  • Improved Code Organization: Modularization allows developers to organize code into smaller, more manageable modules, making it easier to understand and maintain.
  • Flexible Dependency Management: By breaking the application into modules, developers can manage dependencies more granularly, allowing for easier updates, substitutions, and testing of individual components.
  • Reduced Coupling: Modularized dependency injection reduces coupling between different parts of the application, making it easier to change or replace components without affecting other parts of the codebase.
  • Scalability: As the application grows, modularization enables developers to add new features or modules without impacting existing code, promoting scalability and extensibility.
In Flutter, modularized dependency injection typically involves using dependency injection containers or service locators to manage dependencies within each module or feature of the application. Each module defines its own set of services and dependencies, which can be provided and accessed within the module or shared with other modules as needed.
Some popular dependency injection libraries or frameworks for Flutter include:
  • get_it: A simple service locator for Dart and Flutter that allows for easy registration and retrieval of dependencies.
  • provider: A popular state management library for Flutter that also provides dependency injection capabilities through its Provider class.
  • injector: A lightweight dependency injection library for Dart and Flutter that supports modularization and lazy loading of dependencies.
Developers can implement modularized dependency injection in their Flutter apps by:
  • Identifying and defining separate modules or features within the application.
  • Deciding on the scope and lifecycle of dependencies within each module.
  • Using a dependency injection library or framework to register and provide dependencies within each module.
  • Injecting dependencies into classes or widgets as needed using constructor injection or other DI patterns.
Some best practices for using modularized dependency injection in Flutter apps include:
  • Keeping modules small and focused on a single responsibility.
  • Avoiding circular dependencies between modules.
  • Using named or tagged dependencies to differentiate between similar services within a module.
  • Testing each module in isolation to ensure that dependencies are correctly provided and injected.
While modularized dependency injection offers many benefits, it may introduce some complexity, especially in larger applications with many modules and dependencies. Developers should carefully consider the trade-offs and design decisions when implementing modularized dependency injection to ensure that it aligns with the needs and goals of the project.
Developers can find resources and tutorials for implementing modularized dependency injection in Flutter on official documentation provided by Flutter and Dart, community forums like Stack Overflow and GitHub, developer blogs and tutorials, online courses and webinars, and sample projects and code repositories. Additionally, exploring Flutter packages and plugins specific to dependency injection can provide additional insights and guidance for implementation.