Explore Stacked: a Flutter Framework with MVVM design
Simplify dev via code-gen, dependency injection & modularity. Dive deep with examples & harness power for robust apps.
Discover how Stacked, a Flutter Framework based on MVVM design pattern, makes Flutter development easy with its code generation, dependency injection, and modular architecture. We’ll dive into core components, real-world examples, and learn to harness its power for creating robust cross-platform applications.
Choosing the right state management solution is still a hot topic in the Flutter development world. As the popular solutions evolved throughout the years, they slowly turned into full-blown Flutter frameworks.
If you’ve ever been in a dilemma and checked what online articles say, you probably stumbled upon Riverpod or GetX as the main solutions for your app’s state management. They both have a strong user base in Flutter communities. We in Cinnamon Agency’s Flutter development team use those two in almost all of our projects, but some developers don’t find Riverpod and GetX to be the right solution for their project needs. Selecting the right framework (or a state management solution) can make all the difference. After all, it can streamline your development process, improve code readability, and make your application more maintainable, among other things. Today, we're going to dive into one such framework that has been making waves in the Flutter community - Stacked.
Stacked is a Flutter framework that is based on the Model-View-ViewModel (MVVM) design pattern. This might be especially familiar to you if you’ve been developing native Android or Xamarin apps before. In its full form (explanation in the next section), it provides a set of tools that help developers build well-structured and scalable applications. Stacked includes features such as code generation, automatic dependency injection, and easy-to-use navigation and routing. It provides a clear and consistent approach to app architecture, with built-in features like the separation of concerns principle. Furthermore, Stacked is designed to be flexible and extensible, allowing for easy integration with other Flutter packages.
One of the standout features of Stacked is its modularity. It is separated into multiple smaller packages, where each one contributes to the framework’s core set of features. This allows developers to choose which parts of the framework they want to use. As mentioned in the previous section, in its full form, Stacked is packed with useful features, but not all users need all these features. The power of choice is just as important as the tool’s usefulness. With that in mind, the most important packages are the following:
stacked: Core package. It represents the foundation of Stacked on top of which you build everything else.
stacked_generator: A code generation tool allowing developers to automatically generate routes and dependencies.
stacked_hooks: This package provides an integration of Flutter hooks for Stacked. It also introduces StackedHookView<T> which allows a child widget to inherit the parent View’s ViewModel and react to changes from it if necessary.
stacked_services: Provides essential services to aid with navigation, handling dialogs, snackbars, and bottom sheets.
The developer/s behind the Stacked framework (FilledStacks) are very frequently pushing new versions for each of their packages and are constantly improving them. The four packages are the ones that are most likely to be used to harness the full potential of the framework. However, there are many more specifically made to integrate into Stacked. You can find them all on pub.dev.
Stacked framework is built around three main components: View, ViewModel, and Service. Understanding them is crucial to effectively use the framework.
View: Responsible for the UI. It binds to the ViewModel, listens to ViewModel’s state changes, and rebuilds accordingly. The View does not contain any business logic or state - it simply reacts to the state of the ViewModel.
ViewModel: Acts as a bridge between the View and the Service. It handles the state and logic for the View. ViewModel notifies the View of any changes in state, and it communicates with the Service to perform tasks such as fetching data.
Service: As for the services, their use case can vary significantly, but their work is mostly related to data layer communication.
Service’s role might be, from the developer's perspective at least, the most important one in the Stacked framework. It does the business logic’s “heavy lifting”. Its main tasks are API and Database communication on various levels of abstraction. Unlike View and the ViewModel, which are bound together, the Service is independent and can be used by multiple ViewModels. Services can be classified into two categories:
Facade (or Top-level) Services: These services wrap around other packages to remove the hard dependency from our codebase and do the actual work. A very good example of this would be, for instance, a Service that fetches data from an API. The service would be responsible for structuring and making the HTTP request, checking if it was successful, and returning the base response model. You might name it NetworkService, APIService, or something similar to indicate its networking functionalities. This separation of concerns makes the code more readable, maintainable, and testable (the main motivations behind Stacked). It also promotes the DRY (Don't Repeat Yourself) principle, as any ViewModel or Service that requires this functionality can simply locate the service and make use of its functions.
App Services: These services are where your business logic lies. They orchestrate the interaction between Facade Services to complete some domain (business) logic. An App Service example can be a LoginService which checks if a user is logged in, fetch data when a user logs in, and/or save the user’s data to the local database when fetched. This orchestration of business logic makes the feature development process very clear and the code easy to read.
Now that we explained all of the key components and features of Stacked, let's see what it looks like in code using everyone’s go-to example, the counter app. Since this is a relatively simple example, we’ll utilize Stacked’s modularity and only include what we need, when we need it. For starters, we’ll include the core package in our pubspec.yaml. As of writing this article, the latest version of the package is v3.2.7.
In our main.dart, the default MyHomePage StatefulWidget is removed, and, in the MaterialApp, we just specify HomeView as its “home”.
Now we come to the fun part, ViewModel and the View.
ViewModel’s logic is fairly simple. We created a private counter variable to store our counter value and a getter for it to be available outside of the HomeViewModel. An important thing to note is that our HomeViewModel extends the BaseViewModel class which is coming from the core package and gives ViewModel reactive functionalities, mainly the notifyListeners() method. incrementCounter() method increments the value of the counter variable and utilizes notifyListeners() to notify the View of the state change within the HomeViewModel. Let's see how the HomeView which uses this ViewModel will bind and react to changes.
HomeView seems a bit complex at first so we’ll go through it step by step. Observe that HomeView extends class StackedView<T>. It, just like the BaseViewModel, comes from the core package and allows us to “bind” the ViewModel to a View. We do this by first specifying the ViewModel type (<T>) in StackedView.
After the type is specified, we need to override two methods coming from the StackedView class. viewModelBuilder method creates an instance of specified ViewModel type T. In this case, the instance of HomeViewModel.
Next, we override the builder method which gives us access to the HomeViewModel instance created through viewModelBuilder. In it, we create our UI elements and it gets called every time the state of the ViewModel changes.
Accessing the ViewModel’s elements from here is as easy as it gets as you can see in the example of showing the counter value and using the incrementCounter method.
You probably asked yourself, isn’t rebuilding the whole view every time the state of our ViewModel changes, to say the least, not the optimal way of handling the state? Absolutely, in some cases, you might just want the ViewModel to provide you with the data that you would show in the View. No changes, triggers, or reactivity are needed. For this, StackedView comes with another parameter you can override, the reactive getter.
Its usage is as straightforward as it gets, if you want the View to react to ViewModel’s changes, leave it as true (default value), if not, just override the getter to return false.
Rebuilding the whole view every time the ViewModel state changes is not ideal as we already mentioned. For this, Stacked offers a solution with the stacked_hooks package and its StackedHookView<T> widget. It’s an integral part of the Stacked framework. This is how we would extract the Text widget showing updated counter values into its StackedHookView widget.
StackedHookView accepts a type <T> which needs to be the parent’s ViewModel type. It overrides the builder method in which our UI lays. The builder method gets called every time the ViewModel state changes. However, just like with the StackedView, we have the option to override the reactive getter and make the widget non-reactive. When we’re done with creating our reactive widget, including it in the View is as easy as with any other widget.
Now our HomeView is non-reactive and the reactive part of displaying the counter value every time the ViewModel state changes is handled by the CounterWidget. This way, we have optimal state handling without unnecessary rebuilds.
The last thing we need to go through is integrating the Service. To make it simple, we’ll just extract HomeViewModel’s logic and apply it to the newly created CounterService:
In most cases, we want our Service to be reactive. To achieve this, we add ListenableServiceMixin which, among other things, provides us with the notifyListeners() method. As the method name suggests, it notifies all the listeners of this Service of a change.
Services need to be injected as dependencies so we’ll use Stacked recommended get_it package. Implementation of it is simple, we just register the CounterService as a Lazy Singleton through an instance of GetIt.
setUpLocator method needs to be called in main() before runApp().
To allow the ViewModel to react to Service’s changes, we extend it with ReactiveViewModel class, a part of the core package. ReactiveViewModel class provides us with a listenableServices getter. By inserting the Service instance into it, we make ViewModel listen to its changes. In our example, we injected the CounterService as a dependency using GetIt’s locator and made HomeViewModel listen to its changes by registering it in the listenableServices getter.
With this, we went through all of the main Stacked components and their interactions. There was a lot to cover so we didn’t talk about code generation, automatic dependency injection, and many more great features of Stacked so we will cover it in detail in one of the future blogs.
All of the example code can be found in the following GitHub repository: https://github.com/josipbarisic/modular_stacked_tutorial. Feel free to fork it and try things out before we publish a new blog.
Stacked is a powerful Flutter framework that offers a robust set of tools for building well-structured and scalable applications. With its MVVM-inspired architecture, modularity, and reactive functionalities, Stacked provides a clear and consistent approach to app development. While it may seem complex at first, once you grasp its core components and understand their interactions, you'll find that it can significantly streamline your development process. As the Flutter ecosystem continues to evolve, Stacked stands out as a framework that is not only keeping pace but also shaping the future of Flutter development.
Subscribe to our newsletter
We send bi-weekly blogs on design, technology and business topics.