State Management in Flutter: Keeping Your App from Throwing a Temper Tantrum
Introduction
State management can be a perplexing challenge when developing Flutter applications, but fear not! In this comprehensive guide, we’ll unravel the mysteries of state management in Flutter while keeping your app running smoothly and your developers from going bonkers. From built-in options like setState to advanced libraries like Provider, BLoC, Redux, MobX, and GetX, we’ll explore each approach’s strengths, quirks, and humorous anecdotes along the way.
What is State Management?
Imagine a world where your app’s data and user interface live harmoniously. State management refers to the process of managing and updating the data and UI components in an application. In Flutter, state management involves handling changes in the application’s state, such as user input, network responses, or data updates, and updating the UI accordingly. Effective state management ensures your app remains performant, maintainable, and devoid of developer meltdowns.
1. Built-in State Management
Once upon a time, in a land of simplicity, Flutter offered setState. We’ll examine this humble hero and his tales of updating the state and rebuilding the UI. Flutter provides a built-in mechanism for managing state using the setState method. This approach is suitable for small and simple applications with a limited number of UI components. But beware! As your app grows, setState’s limitations might have you pulling out your hair faster than a squirrel on an espresso binge.
@protected
void setState(){
Void Callback fn
}
setState(() {
_myState = newValue;
});
Example of code :
import 'package:flutter/material.dart';
class CounterApp extends StatefulWidget {
@override
_CounterAppState createState() => _CounterAppState();
}
class _CounterAppState extends State<CounterApp> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Counter App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Counter value:',
),
Text(
'$_counter',
style: TextStyle(fontSize: 24),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: Icon(Icons.add),
),
),
);
}
}
void main() {
runApp(CounterApp());
}
In this example, we create a simple counter app that increments a counter value when the floating action button is pressed. The CounterApp widget is a stateful widget that maintains the state of the counter. The counter value is stored in the _counter variable.
When the floating action button is pressed, the _incrementCounter method is called. Inside this method, we use setState to update the state of the _counter variable. The setState function notifies Flutter that the state has changed, and the framework automatically rebuilds the UI with the updated value.
The updated counter value is then displayed in the Text widget inside the Column widget.
By using setState, we ensure that the UI reflects the updated state of the counter and that the app responds to user interactions accordingly.
2. Inheritedwidget and ScopedModel
Prepare to be amazed as we unravel the enigmatic InheritedWidget! Witness its power in sharing state across widget trees, accompanied by ScopedModel. InheritedWidget is a fundamental mechanism in Flutter for sharing state across widget trees. It allows passing down data efficiently from parent widgets to their descendants. ScopedModel, built on top of InheritedWidget, provides a convenient way to manage state by encapsulating the state object and updating the UI whenever the state changes. It is well-suited for small to medium-sized applications with moderate state management requirements.
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
// Define a model class that extends ScopedModel
class CounterModel extends Model {
int _counter = 0;
int get counter => _counter;
void incrementCounter() {
_counter++;
// Notify listeners that the state has changed
notifyListeners();
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// Create an instance of the CounterModel
final CounterModel counterModel = CounterModel();
@override
Widget build(BuildContext context) {
return ScopedModel<CounterModel>(
model: counterModel,
child: MaterialApp(
title: 'ScopedModel Example',
home: CounterApp(),
),
);
}
}
class CounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ScopedModel Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Counter value:',
),
ScopedModelDescendant<CounterModel>(
builder: (context, child, model) {
return Text(
'${model.counter}',
style: TextStyle(fontSize: 24),
);
},
),
],
),
),
floatingActionButton: ScopedModelDescendant<CounterModel>(
builder: (context, child, model) {
return FloatingActionButton(
onPressed: model.incrementCounter,
child: Icon(Icons.add),
);
},
),
);
}
}
In this example, we use the ScopedModel package along with InheritedWidget to manage the state of a counter. The CounterModel class extends Model from ScopedModel and defines the state and logic for the counter.
In the MyApp widget, we create an instance of CounterModel and pass it as the model to the ScopedModel widget. This ensures that the CounterModel is available throughout the widget tree.
In the CounterApp widget, we use ScopedModelDescendant to access the CounterModel instance and rebuild parts of the UI when the state changes. The counter value is displayed using a Text widget, and the FloatingActionButton increments the counter when pressed.
By using ScopedModel and ScopedModelDescendant, we can access the state provided by the CounterModel throughout the widget tree without explicitly passing it down. When the counter is incremented, the notifyListeners() method is called to notify the listeners (in this case, the ScopedModelDescendant widgets), and the UI is automatically updated.
3. Provider Package
Behold the mighty Provider package! Armed with the strength of InheritedWidget and ScopedModel, Provider swoops in to save the day. The Provider Package is a popular state management solution in Flutter. It builds upon the InheritedWidget and ScopedModel approaches and provides a more flexible and scalable solution for managing state. With Provider, you can create providers to expose data or objects to the widget tree and selectively rebuild only the necessary parts of the UI when the state changes. It promotes separation of concerns and reduces boilerplate code, making it suitable for medium to large-scale applications.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Define a model class
class CounterModel with ChangeNotifier {
int _counter = 0;
int get counter => _counter;
void incrementCounter() {
_counter++;
// Notify listeners that the state has changed
notifyListeners();
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => CounterModel(),
child: MaterialApp(
title: 'Provider Example',
home: CounterApp(),
),
);
}
}
class CounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Access the CounterModel instance using Provider.of
final counterModel = Provider.of<CounterModel>(context);
return Scaffold(
appBar: AppBar(
title: Text('Provider Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Counter value:',
),
Text(
'${counterModel.counter}',
style: TextStyle(fontSize: 24),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Call the incrementCounter method on the CounterModel
counterModel.incrementCounter();
},
child: Icon(Icons.add),
),
);
}
}
In this example, we use the Provider package to manage the state of a counter. The CounterModel class is a simple model that extends ChangeNotifier from the provider package.
In the MyApp widget, we create an instance of CounterModel using ChangeNotifierProvider. This provider makes the CounterModel instance available to all the descendant widgets in the widget tree.
In the CounterApp widget, we use Provider.of<CounterModel>(context) to access the CounterModel instance. This way, we can directly retrieve the counter value and call the incrementCounter method on the model.
Whenever the incrementCounter method is called and the counter is updated, the notifyListeners() method is invoked, which notifies the listeners (in this case, the Text widget displaying the counter value), and triggers a UI rebuild.
By using the Provider package, we can easily access the state provided by the CounterModel throughout the widget tree using Provider.of, and the UI automatically updates when the state changes.
4. BLoC (Business Logic Component) Pattern
Enter the BLoC pattern, where business logic meets UI separation. We’ll journey through its unidirectional data flow, streams, and testability. BLoC is an architectural pattern that decouples the business logic from the UI. It uses streams to handle state changes and provides a unidirectional flow of data. The BLoC pattern is a robust solution for complex applications with extensive state management requirements. It promotes testability, code reusability, and a clear separation of concerns.
import 'dart:async';
import 'package:flutter/material.dart';
// Define the BLoC class
class CounterBloc {
int _counter = 0;
// Create stream controllers
final _counterStreamController = StreamController<int>();
final _incrementStreamController = StreamController<void>();
// Create getters for streams
Stream<int> get counterStream => _counterStreamController.stream;
// Constructor
CounterBloc() {
// Listen to the increment stream and update the counter
_incrementStreamController.stream.listen((_) {
_counter++;
// Add the updated counter value to the counter stream
_counterStreamController.sink.add(_counter);
});
}
// Method to dispose of stream controllers
void dispose() {
_counterStreamController.close();
_incrementStreamController.close();
}
// Method to handle increment event
void incrementCounter() {
_incrementStreamController.sink.add(null);
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'BLoC Example',
home: BlocProvider(
bloc: CounterBloc(),
child: CounterApp(),
),
);
}
}
class BlocProvider extends InheritedWidget {
final CounterBloc bloc;
BlocProvider({Key? key, required this.bloc, required Widget child})
: super(key: key, child: child);
static BlocProvider of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<BlocProvider>()!;
}
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) => true;
}
class CounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counterBloc = BlocProvider.of(context).bloc;
return Scaffold(
appBar: AppBar(
title: Text('BLoC Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Counter value:',
),
StreamBuilder<int>(
stream: counterBloc.counterStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(
'${snapshot.data}',
style: TextStyle(fontSize: 24),
);
} else {
return CircularProgressIndicator();
}
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
counterBloc.incrementCounter();
},
child: Icon(Icons.add),
),
);
}
}
5. Redux and MobX
Step into the ring with Redux and MobX! Marvel at Redux’s immutability and unidirectional data flow, while MobX dazzles with its reactive programming prowess. Redux and MobX are popular state management libraries from the broader Dart/Flutter ecosystem. Redux follows strict unidirectional data flow and immutability principles. It is well-suited for large applications with complex state management needs. MobX, on the other hand, provides a reactive programming model and allows for more mutable state management. It is suitable for applications with rapidly changing states.
Example of Redux
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
// Define actions
enum CounterAction { increment }
// Define reducer
int counterReducer(int state, dynamic action) {
if (action == CounterAction.increment) {
return state + 1;
}
return state;
}
void main() {
final store = Store<int>(counterReducer, initialState: 0);
runApp(MyApp(
store: store,
));
}
class MyApp extends StatelessWidget {
final Store<int> store;
MyApp({required this.store});
@override
Widget build(BuildContext context) {
return StoreProvider<int>(
store: store,
child: MaterialApp(
title: 'Redux Example',
home: CounterApp(),
),
);
}
}
class CounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Redux Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
StoreConnector<int, int>(
converter: (store) => store.state,
builder: (context, count) {
return Text(
'Counter value: $count',
style: TextStyle(fontSize: 24),
);
},
),
],
),
),
floatingActionButton: StoreConnector<int, VoidCallback>(
converter: (store) {
return () => store.dispatch(CounterAction.increment);
},
builder: (context, callback) {
return FloatingActionButton(
onPressed: callback,
child: Icon(Icons.add),
);
},
),
);
}
}
In this example, we use the Redux pattern to manage the state of a counter. We define a CounterAction enum that represents the action to increment the counter. The counterReducer function handles the state update based on the action. The main function creates a Redux store with the reducer and initial state.
The MyApp widget wraps the app in StoreProvider, which provides access to the store throughout the widget tree.
In the CounterApp widget, we use StoreConnector to connect the counter value from the Redux store to the UI. The converter function extracts the counter value from the store, and the builder function rebuilds the UI whenever the counter value changes.
The floating action button dispatches the CounterAction.increment action to the store when pressed. The StoreConnector converts the dispatching action into a callback function.
Example of MobX
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
// Define the counter store
class CounterStore = _CounterStore with _$CounterStore;
abstract class _CounterStore with Store {
@observable
int counter = 0;
@action
void incrementCounter() {
counter++;
}
}
void main() {
final counterStore = CounterStore();
runApp(MyApp(
counterStore: counterStore,
));
}
class MyApp extends StatelessWidget {
final CounterStore counterStore;
MyApp({required this.counterStore});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MobX Example',
home: CounterApp(counterStore: counterStore),
);
}
}
class CounterApp extends StatelessWidget {
final CounterStore counterStore;
CounterApp({required this.counterStore});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('MobX Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Observer(
builder: (_) => Text(
'Counter value: ${counterStore.counter}',
style: TextStyle(fontSize: 24),
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
counterStore.incrementCounter();
},
child: Icon(Icons.add),
),
);
}
}
In this example, we employ the MobX package to effectively manage the state of a counter. The CounterStore class serves as the central storage unit, equipped with an observable variable called “counter” and an action method called “incrementCounter()”.
To kickstart the application, we instantiate an object of the CounterStore within the main function.
The MyApp widget takes charge of wrapping the entire app using MaterialApp and ensures that the CounterStore is accessible to the CounterApp.
Inside the CounterApp widget, we leverage the Observer widget from the flutter_mobx library to keep track of changes in the counter value retrieved from the CounterStore. As soon as the counter value undergoes modifications, the user interface (UI) is automatically refreshed.
Whenever the floating action button is pressed, it triggers the execution of the incrementCounter() action method present in the CounterStore. Consequently, the counter value is updated, and MobX’s observables take care of refreshing the UI accordingly.
6. GetX
Prepare to be enchanted by GetX, the lightweight sorcerer of state management. Watch as it effortlessly weaves reactive state management, dependency injection, and routing into your Flutter app. GetX is a lightweight and powerful state management library for Flutter that emphasizes simplicity and high performance. It provides a range of features, including reactive state management, dependency injection, routing, and more. GetX is suitable for both small and large applications and offers a smooth learning curve.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
// Define a controller
class CounterController extends GetxController {
RxInt counter = 0.obs;
void incrementCounter() {
counter.value++;
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final CounterController counterController = Get.put(CounterController());
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'GetX Example',
home: CounterApp(),
);
}
}
class CounterApp extends StatelessWidget {
final CounterController counterController = Get.find();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Obx(() => Text(
'Counter value: ${counterController.counter.value}',
style: TextStyle(fontSize: 24),
)),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
counterController.incrementCounter();
},
child: Icon(Icons.add),
),
);
}
}
In this example, we demonstrate the usage of the GetX package to manage the state of a counter. The CounterController class is responsible for handling the state and consists of an observable variable called “counter” and an action method called “incrementCounter()”.
To incorporate the CounterController into our app, we instantiate an instance of it within the main function and register it with the GetX dependency injection system using Get.put().
The MyApp widget is designed to envelop the entire app using GetMaterialApp. This configuration initializes the GetX framework and ensures that the CounterController is accessible and can be utilized throughout the widget tree.
Inside the CounterApp widget, we leverage the Obx widget provided by GetX to observe any changes in the counter value derived from the CounterController. The user interface (UI) is automatically updated whenever the counter value undergoes modifications.
Upon pressing the floating action button, the incrementCounter() action method of the CounterController is invoked. This results in an update to the counter value and triggers a UI refresh via GetX’s reactive system.
Conclusion
State management may be a formidable foe, but armed with knowledge and a sprinkle of humor, you can conquer it! State management is a critical aspect of building robust Flutter applications. By choosing the right state management technique, you can improve the performance, maintainability, and scalability of your projects. Whether you opt for the built-in solutions like setState, InheritedWidget and ScopedModel, or leverage external libraries like Provider, BLoC, Redux, MobX, or GetX, understanding the strengths and trade-offs of each approach is essential. Consider the complexity of your project, the team’s familiarity with the pattern, and the long-term goals of your application when making your decision. Remember, the key to sanity lies in embracing the strengths and quirks of each approach. So go forth, fluttering developer, and tame the state management beast with confidence and a smile!
About the Author:
Nishant Choudhary is a Software Developer with around 2 years of experience in Software Development where he worked in multiple organizations and contributed to solving different kinds of complex problems with his creativity and enthusiasm for learning and different kinds of skills such as problem-solving, web designing and development, analysis of architecture and development of Progressive Web Applications.
About CodeStax.Ai
At CodeStax.Ai, we stand at the nexus of innovation and enterprise solutions, offering technology partnerships that empower businesses to drive efficiency, innovation, and growth, harnessing the transformative power of no-code platforms and advanced AI integrations.
But the real magic? It’s our tech tribe behind the scenes. If you’ve got a knack for innovation and a passion for redefining the norm, we’ve got the perfect tech playground for you. CodeStax.Ai offers more than a job — it’s a journey into the very heart of what’s next. Join us, and be part of the revolution that’s redefining the enterprise tech landscape.