Welcome to Day 9 of the "Hundred Days of Flutter" course! Today, we'll explore StatefulWidgets and state management, which are essential for creating interactive and dynamic user interfaces in Flutter.
State is the data that can change over time in your widget. Unlike StatelessWidgets, StatefulWidgets can maintain and update their state.
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_count'),
ElevatedButton(
onPressed: _increment,
child: Text('Increment'),
),
],
);
}
}
StatefulWidgets have a lifecycle that includes several important methods:
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
@override
void initState() {
super.initState();
// Called when the widget is first created
// Initialize variables, controllers, etc.
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Called after initState and when dependencies change
// Access inherited widgets, etc.
}
@override
void build(BuildContext context) {
// Called to build the widget
return Container();
}
@override
void didUpdateWidget(MyStatefulWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// Called when the widget is rebuilt with new configuration
// Compare old and new widget properties
}
@override
void dispose() {
// Called when the widget is removed from the tree
// Clean up resources, controllers, etc.
super.dispose();
}
}
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
bool _isLoading = false;
Future<void> _incrementAsync() async {
setState(() => _isLoading = true);
await Future.delayed(Duration(seconds: 1));
setState(() {
_count++;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Count: $_count',
style: Theme.of(context).textTheme.headlineMedium,
),
if (_isLoading)
CircularProgressIndicator()
else
ElevatedButton(
onPressed: _incrementAsync,
child: Text('Increment'),
),
],
);
}
}
class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _submit() async {
if (_formKey.currentState!.validate()) {
setState(() => _isLoading = true);
// Simulate API call
await Future.delayed(Duration(seconds: 2));
setState(() => _isLoading = false);
// Handle successful login
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email'),
validator: (value) {
if (value?.isEmpty ?? true) {
return 'Please enter your email';
}
return null;
},
),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
validator: (value) {
if (value?.isEmpty ?? true) {
return 'Please enter your password';
}
return null;
},
),
if (_isLoading)
CircularProgressIndicator()
else
ElevatedButton(
onPressed: _submit,
child: Text('Login'),
),
],
),
);
}
}
class _MyWidgetState extends State<MyWidget> {
// Only store what needs to change
int _counter = 0;
bool _isLoading = false;
// Don't store what can be computed
int get doubled => _counter * 2;
}
// Good
setState(() {
_counter++;
_isLoading = true;
});
// Bad - multiple setState calls
setState(() => _counter++);
setState(() => _isLoading = true);
class _MyWidgetState extends State<MyWidget> {
late final TextEditingController _controller;
late final StreamSubscription _subscription;
@override
void initState() {
super.initState();
_controller = TextEditingController();
_subscription = myStream.listen(_handleData);
}
@override
void dispose() {
_controller.dispose();
_subscription.cancel();
super.dispose();
}
}
Let's test your understanding of today's concepts:
What is the main difference between StatelessWidget and StatefulWidget?
Which lifecycle method is called when a widget is first created?
What is the purpose of setState in a StatefulWidget?
Create a simple todo list app that demonstrates state management:
Here's a starting point:
class Todo {
final String id;
final String title;
bool completed;
Todo({
required this.id,
required this.title,
this.completed = false,
});
}
class TodoList extends StatefulWidget {
@override
_TodoListState createState() => _TodoListState();
}
class _TodoListState extends State<TodoList> {
final List<Todo> _todos = [];
final _titleController = TextEditingController();
bool _isLoading = false;
String? _error;
@override
void dispose() {
_titleController.dispose();
super.dispose();
}
Future<void> _addTodo() async {
if (_titleController.text.isEmpty) {
setState(() => _error = 'Please enter a title');
return;
}
setState(() {
_isLoading = true;
_error = null;
});
try {
// Simulate network delay
await Future.delayed(Duration(seconds: 1));
setState(() {
_todos.add(Todo(
id: DateTime.now().toString(),
title: _titleController.text,
));
_titleController.clear();
_isLoading = false;
});
} catch (e) {
setState(() {
_error = 'Failed to add todo';
_isLoading = false;
});
}
}
void _toggleTodo(String id) {
setState(() {
final todo = _todos.firstWhere((t) => t.id == id);
todo.completed = !todo.completed;
});
}
void _removeTodo(String id) {
setState(() {
_todos.removeWhere((t) => t.id == id);
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _titleController,
decoration: InputDecoration(
labelText: 'New Todo',
errorText: _error,
),
),
),
if (_isLoading)
CircularProgressIndicator()
else
IconButton(
icon: Icon(Icons.add),
onPressed: _addTodo,
),
],
),
),
Expanded(
child: ListView.builder(
itemCount: _todos.length,
itemBuilder: (context, index) {
final todo = _todos[index];
return ListTile(
leading: Checkbox(
value: todo.completed,
onChanged: (_) => _toggleTodo(todo.id),
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.completed
? TextDecoration.lineThrough
: null,
),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => _removeTodo(todo.id),
),
);
},
),
),
],
);
}
}
Tomorrow, we'll explore navigation and routing in Flutter!
Learn about Flutter's advanced layout widgets including ListView, GridView, and SingleChildScrollView to create scrollable and grid-based user interfaces.
Start Previous DayMaster Flutter's navigation system, including basic navigation, named routes, and passing data between screens to create multi-screen applications.
Start Next Day