- Flutter Development Tutorial (Beginner to Intermediate)
- Part 1: Introduction to Flutter and Environment Setup
- Part 2: Dart Basics for Flutter
- Part 3: Understanding Widgets and Building UI
- Part 4: Navigation and Routing
- Part 5: Working with Forms and Input Handling
- Part 6: Lists, Grids, and Dynamic Content
- Part 7: Advanced State Management
11. Managing Asynchronous Code in Flutter
Flutter uses Dart’s Future and async/await for handling asynchronous tasks such as API calls, file reading, or database access.
11.1 Using Future and async/await
Dart’s Future represents an asynchronous computation that will return a result in the future.
Code Example: Simple Future and async/await
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 2));
return 'Data loaded';
}
class AsyncExample extends StatefulWidget {
@override
_AsyncExampleState createState() => _AsyncExampleState();
}
class _AsyncExampleState extends State<AsyncExample> {
late Future<String> data;
@override
void initState() {
super.initState();
data = fetchData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Async Example')),
body: FutureBuilder<String>(
future: data,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else {
return Center(child: Text('Result: ${snapshot.data}'));
}
},
),
);
}
}
Best Practices for Asynchronous Code
- Use FutureBuilder to display async data in widgets.
- Handle errors properly using try-catch or snapshot.hasError.
- Keep async logic outside the UI layer for better separation of concerns.
12. State Management in Flutter
State management is a key aspect of building dynamic applications where data changes often. In Flutter, managing state is essential for maintaining data between user interactions, screen navigation, and UI updates. There are different ways to manage state depending on the app’s complexity.
12.1 Stateful Widgets (Basic State Management)
The simplest form of state management in Flutter is using Stateful Widgets. These widgets allow you to update the UI dynamically when state changes.
Example: Stateful Counter App
class CounterExample extends StatefulWidget {
@override
_CounterExampleState createState() => _CounterExampleState();
}
class _CounterExampleState extends State<CounterExample> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Counter Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pressed the button this many times:'),
Text('$_counter', style: Theme.of(context).textTheme.headline4),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
How it Works:
- The setState() method triggers the widget tree to rebuild whenever the _counter value changes.
- This is ideal for small, simple applications, but it can become cumbersome in larger apps.
12.2 Lifting State Up
Sometimes, state is shared across multiple widgets. In such cases, “lifting the state up” to a common ancestor widget helps manage the state centrally.
Example: Lifting State Up
class ParentWidget extends StatefulWidget {
@override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool _isActive = false;
void _toggleActive() {
setState(() {
_isActive = !_isActive;
});
}
@override
Widget build(BuildContext context) {
return ChildWidget(
isActive: _isActive,
onToggle: _toggleActive,
);
}
}
class ChildWidget extends StatelessWidget {
final bool isActive;
final VoidCallback onToggle;
ChildWidget({required this.isActive, required this.onToggle});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onToggle,
child: Container(
color: isActive ? Colors.green : Colors.red,
child: Text(isActive ? 'Active' : 'Inactive'),
),
);
}
}
How it Works:
- The parent widget controls the state (_isActive), and passes it down to the child widget along with a callback to modify the state.
- This is effective for scenarios where the state needs to be shared between a few widgets.
12.3 Provider (Advanced State Management)
For larger applications, using a state management solution like Provider is more scalable. Provider allows you to efficiently share state across the widget tree and rebuild only the necessary parts of the UI.
Step 1: Add Provider to pubspec.yaml
dependencies:
provider: ^6.0.0
Step 2: Create a Simple ChangeNotifier Model
class CounterModel with ChangeNotifier {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
notifyListeners();
}
}
Step 3: Provide the Model to the App
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: MyApp(),
),
);
}
Step 4: Use the Model in a Widget
class CounterWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = Provider.of<CounterModel>(context);
return Scaffold(
appBar: AppBar(title: Text('Provider Example')),
body: Center(
child: Text('Counter: ${counter.counter}', style: TextStyle(fontSize: 24)),
),
floatingActionButton: FloatingActionButton(
onPressed: counter.increment,
child: Icon(Icons.add),
),
);
}
}
Best Practices for State Management:
- Use setState() for simple, local state management.
- Use Provider or other solutions (e.g., Riverpod, Bloc) for global state management and more complex applications.
- Avoid lifting state too much as it can make the widget tree difficult to maintain. Use dedicated state management solutions instead.