State Management in Flutter: Enhancing Code Quality and Scalability
Ashish Bhakhand
Senior Software Engineer | Crafting Scalable and Maintainable Systems
This article was originally published on Medium
State management is a crucial concept in Flutter development that, when done right, can significantly improve the quality, maintainability, and scalability of your code. Yet, many developers start with basic state management and gradually realize its limitations as the app grows. In this article, we’ll explore how implementing even basic state management can enhance your Flutter apps. Then, we’ll compare basic state management with more advanced solutions like Provider, BLoC, and Riverpod, demonstrating how each improves code maintainability, scalability, and ease of debugging.
Why State Management Matters
State in a Flutter app represents everything that’s happening in your UI at any given time. This includes user inputs, loading indicators, data fetched from APIs, and more. Managing this state becomes difficult as your app grows in complexity. When state management is handled properly, you can:
Let’s take a look at a common example: fetching and displaying a list of products in a simple shopping app.
The Problem: Basic State Management with setState()
First, let’s start with a basic example using setState(), Flutter's built-in mechanism for state management.
Basic Example: Fetching Products
We’ll create a simple app that fetches a list of products from an API and displays them in a list.
class ProductListScreen extends StatefulWidget {
@override
_ProductListScreenState createState() => _ProductListScreenState();
}
class _ProductListScreenState extends State<ProductListScreen> {
List<String> products = [];
bool isLoading = true;
@override
void initState() {
super.initState();
fetchProducts();
}
Future<void> fetchProducts() async {
// Simulating network request
await Future.delayed(Duration(seconds: 2));
setState(() {
products = ['Laptop', 'Smartphone', 'Headphones', 'Camera'];
isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Products')),
body: isLoading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(products[index]),
);
},
),
);
}
}
The Issues
Now, let’s improve this implementation step by step with different state management solutions.
1. Using Provider for State Management
Provider is a simple and flexible state management solution that decouples your state from the UI, making it easier to manage and test.
Here’s how we can implement the same feature using Provider:
Product Provider
class ProductProvider extends ChangeNotifier {
List<String> products = [];
bool isLoading = true;
Future<void> fetchProducts() async {
await Future.delayed(Duration(seconds: 2));
products = ['Laptop', 'Smartphone', 'Headphones', 'Camera'];
isLoading = false;
notifyListeners();
}
}
Product List Screen
class ProductListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => ProductProvider()..fetchProducts(),
child: Scaffold(
appBar: AppBar(title: Text('Products')),
body: Consumer<ProductProvider>(
builder: (context, provider, child) {
return provider.isLoading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: provider.products.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(provider.products[index]),
);
},
);
},
),
),
);
}
}
领英推荐
Improvements with Provider:
2. Using BLoC for State Management
BLoC (Business Logic Component) is another popular pattern that focuses on making the state predictable by using events and streams.
Here’s how we can implement the same feature using BLoC:
Product BLoC
// Bloc
class ProductBloc extends Cubit<ProductState> {
ProductBloc() : super(ProductInitial());
void fetchProducts() async {
emit(ProductLoading());
await Future.delayed(Duration(seconds: 2));
emit(ProductLoaded(products: ['Laptop', 'Smartphone', 'Headphones', 'Camera']));
}
}
// States
@immutable
abstract class ProductState {}
// Events
class ProductInitial extends ProductState {}
class ProductLoading extends ProductState {}
class ProductLoaded extends ProductState {
final List<String> products;
ProductLoaded({required this.products});
}
Product List Screen
class ProductListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ProductBloc()..fetchProducts(),
child: Scaffold(
appBar: AppBar(title: Text('Products')),
body: BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
if (state is ProductLoading) {
return Center(child: CircularProgressIndicator());
} else if (state is ProductLoaded) {
return ListView.builder(
itemCount: state.products.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(state.products[index]),
);
},
);
}
return Container();
},
),
),
);
}
}
Improvements with BLoC:
3. Using Riverpod for State Management
Riverpod is a powerful and flexible state management solution that provides better performance and more features compared to Provider.
Here’s how we can implement the same feature using Riverpod:
Product Provider with Riverpod
final productProvider = StateNotifierProvider<ProductNotifier, ProductState>((ref) {
return ProductNotifier();
});
class ProductNotifier extends StateNotifier<ProductState> {
ProductNotifier() : super(ProductLoading());
void fetchProducts() async {
await Future.delayed(Duration(seconds: 2));
state = ProductLoaded(products: ['Laptop', 'Smartphone', 'Headphones', 'Camera']);
}
}
@immutable
abstract class ProductState {}
class ProductLoading extends ProductState {}
class ProductLoaded extends ProductState {
final List<String> products;
ProductLoaded({required this.products});
}
Product List Screen
class ProductListScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(productProvider);
return Scaffold(
appBar: AppBar(title: Text('Products')),
body: state is ProductLoading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: (state as ProductLoaded).products.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(state.products[index]),
);
},
),
);
}
}
Improvements with Riverpod:
Conclusion
Each state management solution — Provider, BLoC, and Riverpod — offers significant improvements over basic state management (setState()) by providing better separation of concerns, scalability, testability, and debugging capabilities. While Provider is great for simple applications, BLoC and Riverpod provide more structure and scalability for larger apps.
By implementing proper state management, you can improve the quality of your Flutter code, make it easier to maintain, and prevent bugs before they happen.
Which state management solution is right for you? It depends on your app’s complexity and your team’s preferences. But one thing is for sure: using structured state management will lead to cleaner, more maintainable Flutter apps.