Introduction
Architecture in programming is a diverse topic with various solutions aimed at fixing different issues. App Architecture, especially, is a realm where personal choices matter. If an approach meets your needs and helps release quality code, it's worth considering.
This article introduces Stacked Architecture, a clean and efficient way to structure your next Flutter app. It encompasses Dependency Injection, ready-to-use services, and layered structures following Clean Architecture principles.
Developed by Dane Mackier from FilledStacks, Stacked Architecture offers an MVVM solution packed with components for building scalable, maintainable, and usable Flutter applications. It covers State Management, Dependency Injection, Navigation Abstraction, Out-of-the-box Services, and more.
Scalability is a core focus of Stacked, ensuring your team remains efficient. It provides clear guidance on code conventions and feature development, allowing easy addition and maintenance of functionalities.
Testability is another pillar of Stacked's approach. Its MVVM architecture simplifies unit testing of business logic or state, promoting a robust testing environment.
Maintainability is ensured through strong separation of concerns and strict coding principles, facilitating consistent code scaling without the risk of becoming unmanageable.
In essence, Stacked Architecture and the Stacked framework empower developers to create scalable, testable, and maintainable Flutter applications, backed by a comprehensive set of tools and principles.
Get Started
To get started with Stacked, install the stacked_cli package using pub by running:
dart pu b global activate stacked_cli
This will give you access to all the Stacked goodies.
Create a Stacked App
To create your first app, run:
stacked create app my_first_app
The library creator also provides multiple functionalities that make development much easier. It is known as stacked_services. It contains implementations of Navigation, Dialog, and Snackbar that go perfectly with the Stacked architecture.
This command sets up your Stacked Flutter application. Connect a device or an emulator and launch the app using the standard Flutter command:
flutter run
Upon execution, you'll encounter a loading screen featuring an indicator, followed by a View showcasing a counter and buttons. This initial setup provides the fundamental elements essential for a Stacked application. By default, it includes:
State management
Start-up logic functionality
Navigation
Dialog UI builders
BottomSheet UI builder
Dependency Inversion
Unit tests example
Everything that you need to build a production flutter app with your team.
How Does it Work
Stacked implements separation of concerns and dependency injection by building Flutter code around 3 entities: Views, ViewModels, and Services.
Views — handle only UI code and are linked to ViewModels.
ViewModels — accompany views and handle UI logic. ViewModels use services. Views should never access services.
Service — A specialty of this architecture. It encapsulates all the shared functionalities like native plugins, third-party libraries (like Firebase), and business logic, and it also can be used to transfer data between ViewModels.
Let's Try it Out
let's add a fresh View called "home." I know, not the most inventive name, but it's a simple way to demonstrate Stacked's essentials. To generate this new View using Stacked, use the following command:
stacked create view home
This command will create three files for us:
home_view.dart: This is where you build your UI using Flutter widgets.
home_viewmodel.dart: Store state and perform actions for users as they interact.
home_viewmodel_test.dart: Contains all the unit tests for the HomeViewModel.
Let's dissect the View first.
View
This is the space where the UI code lives. In Stacked, we don't use StatelessWidget or StatefulWidget as our base. Instead, we extend from a StackedView.
class HomeView extends StackedView<HomeViewModel> {
@override
// A builder function that gives us a ViewModel
Widget builder(
BuildContext context,
HomeViewModel viewModel,
Widget? child,
) {
return Scaffold(
...
);
}
@override
HomeViewModel viewModelBuilder(BuildContext context) => HomeViewModel();
}
Additionally, there's a function called viewModelBuilder that's essential. It helps make our ViewModel, which keeps track of what's happening in our app. But before we get into that, let me explain how Views and ViewModels work together. This is the core of how Stacked manages things behind the scenes. StackedView's main job is to link our ViewModel with what we see on the screen. This separation lets us keep our code organized—putting all the stuff about how things work apart from how they look. It's pretty straightforward!
Build the UI from the ViewModel, update the ViewModel, and then rebuild the UI from that ViewModel.
ViewModel
The generated ViewModel is quite straightforward—it's a simple class that extends from BaseViewModel:
class HomeViewModel extends BaseViewModel {}
Managing the state is so simple. For instance, in our HomeView, let's say we want to change the displayed name on the UI when a button is pressed. To do this, we'll create a function that updates the name. Once we've updated the name, we call rebuildUi
, which triggers our builder function in the View:
class HomeViewModel extends BaseViewModel {
String _userName = 'Mohsin';
String get userName => _userName;
void updateName(String name) {
_userName = name
rebuildUi();
}
}
Services
Services are defined by their unique functions. This approach offers flexibility and benefits by encapsulating specific functionalities within a class.
We generate our service by running the following command:
stacked create service authentication
This will create the service and register it for dependency inversion. where we'll add a new function to check if the user is logged in. For now, this will return a static value for simplicity's sake:
class AuthenticationService {
bool userLoggedIn() {
return true;
}
}
Startup Logic
Let's open the startup_viewmodel.dart file to initiate our startup logic for the app. As previously discussed, our goal is to determine whether the user is logged in. If they are, the app navigates to the HomeView; otherwise, it navigates to the LoginView. This is represented in the following code:
class StartupViewModel extends BaseViewModel {
// 1. Get the Authentication and NavigationService
final _authenticationService = locator<AuthenticationService>();
final _navigationService = locator<NavigationService>();
Future runStartupLogic() async {
// 2. Check if the user is logged in
if (_authenticationService.userLoggedIn()) {
// 3. Navigate to HomeView
_navigationService.replaceWith(Routes.homeView);
} else {
// 4. Or navigate to LoginView
_navigationService.replaceWith(Routes.loginView);
}
}
}
Initially, we acquire the essential services—AuthenticationService, which we've created, and NavigationService, which is a component of Stacked. Then, by assessing the user's login status, we decide whether to navigate to the HomeView or the LoginView. It's a straightforward process. Running this code will lead you to the purple View if the user is logged in. If you change the value in AuthenticationService to false, upon restart, the app will direct you to the red View. This covers the fundamental aspects of the Startup code in Stacked.