Why does state management play a key role in Flutter?
The state can be understood as data or information in the app that determines its display and behavior.
As Flutter is a reactive framework, the app’s UI is a direct reflection of its state.
Therefore, efficient state management is paramount for building efficient, reliable Flutter applications.
What’s the Relationship Between State Management and Classic Architecture?
State management is a crucial part of software architecture.
It handles the flow and changes of data within a program.
This includes how data is stored in memory, how data changes, and how data is passed between different parts of the program.
Classical architectures such as MVC (Model-View-Controller) and MVVM (Model-View-ViewModel) incorporate state management.
For instance, in MVC, the Model manages the state of the program, the View displays the state, and the Controller manages user input and updates the model.
However, as front-end applications have become more complex and dynamic, we’ve found a need for more specialized and advanced state management tools and techniques, like Redux, Bloc, etc.
These new state management tools are not mutually exclusive with classic software architectures; in fact, they are often used in combination to handle the complex state management issues in modern applications.
The Evolution of Classical Architecture
MVC (Model-View-Controller) — This is the earliest design pattern, introduced in the 1970s by Trygve Reenskaug in the Smalltalk-76 project.
Origin & Concept: MVC is one of the earliest design patterns in software engineering. It divides the application into three interconnected components:
- The Model represents the data and the business logic.
- The View is the user interface.
- The Controller acts as an intermediary between the model and the view.
Limitations: While MVC brought a structured approach to software design, it had its challenges:
- Scalability: As applications grew, controllers often became bloated, making them hard to maintain.
- Testability: Tight coupling between components made it difficult to test them independently.
class MyModel {
String data;
MyModel(this.data);
}
class MyController {
final MyModel model;
MyController(this.model);
void updateData(String newData) {
model.data = newData;
// Update the UI somehow
}
}
class MyView extends StatelessWidget {
final MyController controller;
MyView(this.controller);
@override
Widget build(BuildContext context) {
return Text(controller.model.data);
}
}
MVP (Model-View-Presenter) — This pattern, an improvement over MVC, was used by Taligent in their CommonPoint environment in the mid-1990s and became widely used after the introduction of Google Web Toolkit in 2006.
Evolution & Differences: To address MVC’s shortcomings, MVP was introduced.
- The Presenter replaces the controller. Unlike the controller, it dictates the logic to the view, which is now passive.
- This change allowed for better separation of concerns and made unit testing more feasible.
Limitations: MVP improved upon MVC, but issues persisted:
- Complexity: For large applications, presenters could become complex.
- View-Presenter Communication: The communication was still overly complex in dynamic applications.
class MyModel {
String data;
MyModel(this.data);
}
class MyPresenter {
final MyModel model;
MyView view;
MyPresenter(this.model);
void updateData(String newData) {
model.data = newData;
view.updateDisplay(model.data);
}
}
abstract class MyView {
void updateDisplay(String data);
}
MVVM (Model-View-ViewModel) — This pattern was first introduced by Microsoft around 2005 and has been widely used in Silverlight and WPF.
Further Evolution: MVVM emerged from the need to simplify the interaction between the user interface and its underlying data/logic.
- The ViewModel serves as an abstraction of the view, exposing methods, commands, and other properties that help maintain the view’s state.
- It relies heavily on data-binding, reducing the need for explicit manipulation of the view.
Limitations: Despite its advantages, MVVM isn’t without its flaws:
- Overhead: The data-binding mechanism can be complex and introduce overhead.
- Learning Curve: Understanding and implementing MVVM effectively can be challenging.
class MyModel {
String data;
MyModel(this.data);
}
class MyViewModel {
final MyModel model;
String get data => model.data;
MyViewModel(this.model);
}
class MyView extends StatelessWidget {
final MyViewModel viewModel;
MyView(this.viewModel);
@override
Widget build(BuildContext context) {
return Text(viewModel.data);
}
}
MVI (Model-View-Intent) — MVI has become popular in recent years and is introduced by Android developer community.
Latest Evolution for Reactive Programming: MVI is a more recent pattern, often associated with reactive programming.
- Model: Holds the app’s state.
- View: Displays the state to the user.
- Intent: Represents the user’s intentions, triggering state changes.
class MyModel {
String data;
MyModel(this.data);
}
class MyView extends StatelessWidget {
final MyModel model;
MyView(this.model);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(model.data),
ElevatedButton(
onPressed: () {
// Send intent to update the model
},
child: Text('Update Data'),
),
],
);
}
}
Why MVI Suitable for Flutter
- Unidirectional Data Flow: MVI’s linear and predictable data flow aligns well with Flutter’s reactive and widget-centric architecture.
- State Management: MVI simplifies state management, a critical aspect in Flutter, where the UI reacts to state changes.
- Immutability and Reactivity: The pattern embraces immutability and reactivity, which are key principles in Flutter’s design.
Limitations:
- Complexity: The concept of intents and a strict unidirectional flow can be initially challenging to grasp.
- Boilerplate: MVI can introduce additional boilerplate, especially for simple scenarios.
MVI State Management Approach
- Maturity of the Framework
- Community Support
- Ease of Use
- Compatibility with Existing Flutter Architecture
Provider vs Riverpod
Provider
Advantages
- Simplicity: Provider’s API is simple and intuitive, especially for beginners.
- Community Support: As an early library, it has extensive community support and abundant learning resources.
- Tightly integrated with Flutter: It is completely dependent on the Flutter framework, with no need for additional abstraction layers.
Disadvantages
- Testability: In some cases, Provider may not be as testable as Riverpod.
- Flexibility: Provider may not be as flexible as Riverpod when handling more complex state management scenarios.
Riverpod
Advantages
- Greater flexibility: Riverpod offers more abstraction, making state management in complex applications more flexible.
- Testability: Riverpod was designed with testability in mind, making state testing easier to implement.
- Decoupled from Flutter: It does not depend on BuildContext, so it can be used more flexibly in non-UI logic.
- Safer state management: Riverpod aims to reduce runtime errors through compile-time safety.
Disadvantages
- Learning curve: For beginners, due to its high level of abstraction, Riverpod’s learning curve is steeper than Provider’s.
- Relatively new: Compared to Provider, Riverpod has less community support and resources.
Winner: Riverpod
BLoC vs Mobx
BLoC
Advantages
- Predictable State Management: Built on the BLoC (Business Logic Component) pattern, it encourages a unidirectional data flow, which enhances predictability and maintainability.
- Separation of Concerns: Encourages a clear separation between the presentation and business logic, facilitating easier testing and maintenance.
- Scalability: Well-suited for large applications where you need a robust solution for managing complex state.
- Stream-based Architecture: Leverages Dart’s streams and StreamBuilder for state management, offering fine-grained control over the state and its changes.
- Community Support and Documentation: Has a strong community support and extensive documentation, making it easier for developers to get started and find solutions to common problems.
Disadvantages
- Boilerplate Code: Requires more boilerplate code compared to some other state management solutions, which might slow down development in smaller projects.
- Learning Curve: The concepts of streams and BLoC pattern can be challenging for beginners to grasp.
- Verbose for Simple Use Cases: Might be overkill for very simple applications, where simpler state management solutions could suffice.
Mobx
Advantages
- Reactive State Management: Based on the MobX principles, it provides a transparently reactive way to manage your application’s state.
- Less Boilerplate: Generally requires less boilerplate than bloc, allowing for quicker development cycles, particularly in smaller applications.
- Fine-grained Reactions: Automatically tracks changes in the state and updates the UI in a fine-grained manner, leading to efficient rendering.
- Simplicity and Intuitiveness: Offers a straightforward and intuitive API, which can be easier to grasp for developers with less experience in reactive programming.
- Observables and Actions: Clear distinction between observables (state) and actions (methods that modify state), aiding in structuring the code effectively.
Disadvantages
- Over-reliance on Code Generation: Relies heavily on code generation, which can sometimes lead to confusion and difficulties in debugging.
- Not as Scalable as BLoC: While suitable for many applications, it might not be as scalable as bloc for managing complex and large-scale state.
- Less Predictability: The reactive nature might make the data flow less predictable compared to the more structured approach of bloc.
Winner: BLoC
GetIt vs. GetX
GetIt
get_it
is essentially a service locator for Dart, providing a centralized way to access your classes and services.
Advantages
- Decoupling:
get_it
promotes a high degree of decoupling between your UI and business logic. It allows you to access your objects from anywhere in the app without context. - Testability: Due to its nature, it makes unit testing easier. You can replace your real services with mock services when testing.
- Flexibility:
get_it
doesn't enforce any architectural pattern, offering flexibility in how you structure your code.
Disadvantages
- Manual Wiring: You need to manually manage the lifecycle of your objects and dependencies.
- Lack of Integrated State Management:
get_it
is primarily a service locator and doesn't offer built-in state management solutions, often requiring integration with other packages likeprovider
orbloc
.
GetX
GetX is an extra-light and powerful solution for Flutter. It combines high-performance state management, intelligent dependency injection, and route management quickly and practically.
Advantages
- All-in-One Package: GetX is a comprehensive solution that includes state management, dependency injection, and route management, reducing the need to use multiple different packages.
- Reactive State Management: It allows for easy and efficient reactive programming.
- Simplicity and Readability: GetX is known for its simple and readable syntax, potentially speeding up development time.
- Resource Efficiency: GetX claims to be more resource-efficient compared to other state management solutions, which can be beneficial for more complex applications.
Disadvantages
- Overhead: For smaller projects, the extensive features of GetX might be overkill, leading to unnecessary complexity.
- Learning Curve: Understanding all aspects of GetX can be challenging for beginners, particularly if they are not familiar with reactive programming.
- Less Flexibility in Architectural Choices: Since GetX is an all-in-one solution, it might enforce certain patterns and practices, which can limit architectural choices.
Winner: Neither
Riverpod vs. BLoC
Riverpod
Maintainer: Riverpod is maintained by Rémi Rousselet, who is also the author of the well-known provider package. He has a high reputation in the Flutter community with a deep understanding of state management.
Follower Count: With 32.1K followers, Riverpod is very popular in the Flutter community, especially considering it’s a relatively new library.
Affiliated Organization: Riverpod is maintained by the Invertase organization, which is famous for projects such as Globe, Melos, FlutterFire, and Custom Lint. The diversity and quality of these projects indicate that there is a strong and diversified organization behind Riverpod.
BLoC
Maintainer: BLoC is maintained by Felix Angelov and is part of the Shortbirds organization. Felix also enjoys a reputation in the Flutter community, particularly in the popularization and development of the BLoC pattern.
Follower Count: BLoC has 16.5K followers, and although it’s less than Riverpod, this number still indicates BLoC has significant influence and recognition among Flutter developers.
Affiliated Organization: BLoC is maintained by the Shortbirds organization, which also maintains projects such as Shortbirds and BrickHub, indicating that the BLoC maintenance team focuses on Flutter-related tools and frameworks.
Comparison
Community Impact: Riverpod appears to have a broader influence in the Flutter community, which can be seen from its larger follower count.
Background and Diversity: Riverpod is maintained by an organization that has been deeply involved in multiple Flutter projects. This might mean it has a wider perspective on applicability and innovation. BLoC, on the other hand, is more focused on Flutter’s state management and the BLoC pattern.
Innovation and Development: As the author of the provider, Rémi Rousselet’s profound understanding of Flutter state management might have positively impacted Riverpod’s development. Conversely, Felix Angelov and his team are primarily dedicated to the optimization and promotion of the BLoC pattern.
Small to medium-sized teams, especially those that are technically proficient and flexible, might find Riverpod more suitable for their needs.
Large teams, particularly those with strict requirements on structure and consistency, might lean towards using BLoC.
Winner: Both
MVI with Riverpod
Model
@immutable
class TodoModel {
final List<TodoItem> items;
TodoModel({this.items = const []});
}dddddd
State Notifier
class TodoNotifier extends StateNotifier<TodoModel> {
TodoNotifier() : super(TodoModel());
void addTodoItem(TodoItem item) {
state = TodoModel(items: [...state.items, item]);
}
}
Provider
final todoProvider = StateNotifierProvider<TodoNotifier, TodoModel>((ref) {
return TodoNotifier();
});
View
class TodoListView extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final todoModel = ref.watch(todoProvider);
return ListView.builder(
itemCount: todoModel.items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(todoModel.items[index].title),
);
},
);
}
}
Intent
FloatingActionButton(
onPressed: () {
ref.read(todoProvider.notifier).addTodoItem(TodoItem('New Item'));
},
child: Icon(Icons.add),
)
Riverpod Best Practice
- It is possible to use Riverpod without Flutter.
- Inside the build method, use “watch”. Inside click handlers and other events, use “read”. When in need of filtering out values and rebuilds, use “select”.
- Riverpod strongly recommends enabling lint rules (via
riverpod_lint
). - Providers should exclusively be top-level final variables.
Mastering State: Elevate Your Flutter Development.