Explore state management in Flutter, learn about stateful and stateless widgets, and build a practical To-Do List App to apply state management techniques effectively.
State management is a crucial aspect of Flutter development, enabling developers to handle and maintain the state of their applications efficiently. This chapter introduces the fundamental concepts of state management, explores various techniques, and guides you through building a practical To-Do List App to apply what you’ve learned.
In the context of Flutter, “state” refers to any data that can change over time. This includes user inputs, fetched data, or any variable that affects the UI. Managing state effectively is essential for creating responsive and dynamic applications.
Flutter applications are built using widgets, which can be either stateful or stateless.
Stateless Widgets: These widgets do not store any state. They are immutable and are rebuilt every time they need to be displayed. Use them when the UI does not change dynamically.
class MyStatelessWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('I am a stateless widget');
}
}
Stateful Widgets: These widgets can store state and are mutable. They can change dynamically based on user interaction or other events.
class MyStatefulWidget extends StatefulWidget {
@override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Counter: $_counter'),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment'),
),
],
);
}
}
Understanding the lifecycle of stateful widgets is crucial for managing state effectively. The lifecycle consists of several stages:
graph TD; A[createState()] --> B[initState()]; B --> C[build()]; C --> D[setState()]; D --> C; C --> E[dispose()];
The setState
method is pivotal in updating the UI in response to state changes. It notifies the framework that the state has changed, prompting a rebuild of the widget.
Usage: Call setState
within a stateful widget to update the UI.
void _updateState() {
setState(() {
// Update state variables here
});
}
In Flutter, it’s common to share state between widgets. This is often achieved by “lifting state up” to a common ancestor widget that manages the state and passes it down to child widgets.
Data can be passed between widgets using constructors. This is a straightforward way to share state between parent and child widgets.
class ParentWidget extends StatelessWidget {
final String data = 'Hello from Parent';
@override
Widget build(BuildContext context) {
return ChildWidget(data: data);
}
}
class ChildWidget extends StatelessWidget {
final String data;
ChildWidget({required this.data});
@override
Widget build(BuildContext context) {
return Text(data);
}
}
Callbacks are functions passed from parent to child widgets to handle events or actions. They allow child widgets to communicate with their parents.
class ParentWidget extends StatelessWidget {
void _handleButtonPress() {
print('Button pressed in child widget');
}
@override
Widget build(BuildContext context) {
return ChildWidget(onPressed: _handleButtonPress);
}
}
class ChildWidget extends StatelessWidget {
final VoidCallback onPressed;
ChildWidget({required this.onPressed});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
child: Text('Press me'),
);
}
}
Inherited widgets provide a way to efficiently propagate state down the widget tree. They are particularly useful for sharing state with many descendant widgets.
of(context)
to access it from descendant widgets.class MyInheritedWidget extends InheritedWidget {
final int data;
MyInheritedWidget({required this.data, required Widget child}) : super(child: child);
@override
bool updateShouldNotify(MyInheritedWidget oldWidget) {
return oldWidget.data != data;
}
static MyInheritedWidget? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
}
}
The Provider package is a popular choice for state management in Flutter. It offers a simple and efficient way to manage state across your application.
Setup: Add the Provider package to your pubspec.yaml
file.
dependencies:
provider: ^6.0.0
Usage: Wrap your widget tree with a ChangeNotifierProvider
and use Consumer
or Provider.of
to access the state.
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => Counter(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterScreen(),
);
}
}
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = Provider.of<Counter>(context);
return Scaffold(
appBar: AppBar(title: Text('Counter')),
body: Center(
child: Text('Count: ${counter.count}'),
),
floatingActionButton: FloatingActionButton(
onPressed: counter.increment,
child: Icon(Icons.add),
),
);
}
}
ScopedModel is another state management solution that provides a simple way to manage state across your app. It is less commonly used than Provider but still effective for smaller applications.
Setup: Add the ScopedModel package to your pubspec.yaml
file.
dependencies:
scoped_model: ^1.0.1
Usage: Define a model class extending Model
, and use ScopedModel
and ScopedModelDescendant
to provide and access the model.
class CounterModel extends Model {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
notifyListeners();
}
}
void main() {
runApp(
ScopedModel<CounterModel>(
model: CounterModel(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterScreen(),
);
}
}
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ScopedModelDescendant<CounterModel>(
builder: (context, child, model) {
return Scaffold(
appBar: AppBar(title: Text('Counter')),
body: Center(
child: Text('Counter: ${model.counter}'),
),
floatingActionButton: FloatingActionButton(
onPressed: model.increment,
child: Icon(Icons.add),
),
);
},
);
}
}
Choosing the right state management technique depends on the complexity and requirements of your application.
Consumer
or Selector
to minimize widget rebuilds in Provider.In this project, you’ll build a simple To-Do List App to practice state management techniques. The app will allow users to add, update, and remove tasks.
Start by designing a basic UI for the To-Do List App. The app will consist of a list of tasks and a form to add new tasks.
ListView
.TextField
and a button to add new tasks.Use the Provider package to manage the state of the tasks.
class Task {
String title;
bool isCompleted;
Task({required this.title, this.isCompleted = false});
}
class TaskProvider with ChangeNotifier {
List<Task> _tasks = [];
List<Task> get tasks => _tasks;
void addTask(String title) {
_tasks.add(Task(title: title));
notifyListeners();
}
void toggleTaskCompletion(int index) {
_tasks[index].isCompleted = !_tasks[index].isCompleted;
notifyListeners();
}
void removeTask(int index) {
_tasks.removeAt(index);
notifyListeners();
}
}
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => TaskProvider(),
child: MyApp(),
),
);
}
class ToDoListScreen extends StatelessWidget {
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
final taskProvider = Provider.of<TaskProvider>(context);
return Scaffold(
appBar: AppBar(title: Text('To-Do List')),
body: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: taskProvider.tasks.length,
itemBuilder: (context, index) {
final task = taskProvider.tasks[index];
return ListTile(
title: Text(task.title),
trailing: Checkbox(
value: task.isCompleted,
onChanged: (value) {
taskProvider.toggleTaskCompletion(index);
},
),
onLongPress: () {
taskProvider.removeTask(index);
},
);
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(labelText: 'Add a task'),
),
),
IconButton(
icon: Icon(Icons.add),
onPressed: () {
if (_controller.text.isNotEmpty) {
taskProvider.addTask(_controller.text);
_controller.clear();
}
},
),
],
),
),
],
),
);
}
}
For this project, data persistence is not implemented, but you can extend the app to use local storage solutions like Shared Preferences or SQLite to save tasks between sessions.
State management is a fundamental concept in Flutter development. By understanding and applying the techniques covered in this chapter, you can build responsive and dynamic applications. The To-Do List App project provides a practical example of how to manage state effectively using the Provider package.
For further exploration, consider diving into more advanced state management solutions like Riverpod, Redux, or BLoC. These tools offer additional features and patterns for managing complex state in larger applications.