Stacked Framework: Critique & Ways to Improve
Stacked
Many people follow the official Flutter channel, and there is a well-known rubric called Observable Flutter, which invites the most renowned contributors and open-source activists.
Recently, Dane Mackier, the author of Stacked, presented the library and I aim to provide an overview of his brainchild. My objective is to inform and increase awareness of the architecture and principles presented, rather than only critique. Additionally, I anticipate that the author will use my writing to enhance the appeal of his creation for end-users.
Stacked refers to a comprehensive framework. It is noted that this concept cannot withstand criticism from the outset. Bob Martin once written in his Clean Architecture:
> "Don’t force users of a component to depend on things they don’t need."
> "Gather into components those classes that change for the same reasons and at the same times. Separate into different components those classes that change at different times and for different reasons."
When incorporating a state-management library like BLoC, one would expect to solely focus on state management. However, with the addition of stacked, a framework is introduced that not only provides state management, but also includes architecture, navigation, and forms. Stacked sets its own parameters, concepts and objectives, resulting in a consolidated toolset.
MVVM
The stacked architectural approach embodies the MVVM pattern, comprising of a Model (the data source), a ViewModel (which binds the Model and View), and a View (the layout). This is a widely adopted and effective approach for separating logic and layout in numerous applications, including extensive and resilient ones. The stacked naming convention deviates slightly, using "Service" instead of "Model".
Problems with model
The model layer is responsible for providing data to the view model, which mirrors the function of the service layer in stacked. However, NavigationService, DialogService, and similar services appear to mix in UI logic with models, which is an unusual practice for a layer tasked with providing data. This is detrimental as it results in a convoluted code, i.e. one that is neither maintainable nor modular. To improve the situation, one can establish stringent layer guidelines, such as:
- Services are used solely for providing data and cannot be utilised for any UI-related tasks.
- ViewModels are used for, and only for business logic. They know how to work with models and they know the interests of views.
- Views are solely utilised for painting, laying out and presenting data from the View Model, with no other intended purposes.
Navigation Service
Now that I have discussed navigation, it is important to consider how to enhance it. It is recommended that GlobalKeys are not used for navigation as they allow for rule-breaking, resulting in decisions like NavigationService, which is a poor choice. The issue with NavigationService is that the BuildContext associated with the global key can be accessed from anywhere, even in a Service.
To fix this, every ViewModel ought to have access to the context of a linked "View." Consequently, navigating without breaching build context isolation guidelines whilst having a direct dependency is effortless. To illustrate, refer to https://pub.dev/packages/elementary.
Dependency Injection & Service Locator
The stacked library utilises a service locator named get_it to address certain issues. While this approach may seem appealing, it suffers from a variety of drawbacks.
This anti-pattern registers all the modules in one location and subsequently delivers them. This is achieved by using global variables which include a hashtable-like structure. Problems with service locator include:
- Dependencies become hidden (or indirect). This implies that you can create an instance of a class without directly supplying dependencies (via constructor). Essentially, they are accessed from the service locator itself, violating the dependency injection pattern.
- Runtime resolution - using service locators means that you may not realise that you have failed to provide a dependency until it is actually used.
- Modules are harder to test due to hidden dependencies which can make it difficult to determine which dependencies are necessary for a module to function correctly. Mocking or stubbing can be cumbersome. When testing modules that use service locators, one may need to set up and tear down the global service registry or container, which could potentially affect other tests running in the same environment.
- Affects scalability and maintainability as this approach encourages violating the natural flow of dependencies (via constructors), enabling access from anywhere.
To improve the situation, it is recommended to avoid using the Service Locator and instead use the dependency injection technique. This approach can be observed in my Flutter Starter - https://github.com/hawkkiller/sizzle_starter?tab=readme-ov-file#initialization. With this method, engineers are encouraged to develop stable and reliable systems.
Dependency Inversion Principle
Unfortunately, I failed to observe the implementation of interfaces in Stacked. To put it simply, without interfaces, your components become tightly coupled with concrete classes. This can significantly hinder the creation of tests, implementation of changes, introduction of new features, and bug fixes.
The Dependency Inversion Principle is a part of Robert Martin's SOLID principles. According to the principle, "Components should depend upon abstractions rather than concrete classes". To understand the SOLID principles, it is advisable to read my detailed article on the principles, which includes numerous examples and precise definitions. You can find it at https://lazebny.io/solid.
From the outset, it is important to have a clear understanding of the precise item you are constructing. It is advisable to commence with designing interfaces and generating a class diagram. This approach will enable your components to no longer rely on specific components. This is highly advantageous since you can formulate mock versions for testing purposes, undertake targeted modifications in the future, and this consequently minimises coupling.
Navigation & Routing
Stacked includes its own routing solution using codegen and Navigator 2. However, if you examine the source code of AutoRoute (https://github.com/Milad-Akarie/auto_route_library/tree/master/auto_route/lib/src/router) and Stacked (https://github.com/Stacked-Org/stacked/tree/master/lib/src/router), you will notice a very similar layout. This is because the author of Stacked duplicated the popular AutoRoute library, simply changing all references to AutoRoute with Stacked.
Unfortunately, I could not find any reference indicating that Stacked employs AutoRoute sources. As open-source contributors, it is important to acknowledge and honour one another's work. If we utilise another's code, mentioning it should be the minimum mark of respect.
Thanks for reading me
I hope this brief overview was helpful. I would like to remind you that my intention was not criticism. The promotion of destructive ideas is not something I enjoy. I have participated in projects that were incredibly difficult to maintain, which is why I strive to improve myself and share my ideas. Consider supporting me by subscribing to my telegram channel and LinkedIn to not to miss exciting things about Dart & Flutter.