Skip to content

Flutter - State Management

Managing state – the data that determines what the UI displays and how it behaves – is a fundamental aspect of application development. As applications grow, efficiently handling state becomes critical for maintainability and performance.

Consider a shopping app:

  • User login status needs to be accessible across multiple screens.
  • The contents of the shopping cart need to persist as the user navigates between product pages and the cart screen.
  • A product’s rating might only be relevant on its detail page.
  • The state of an ‘add to cart’ animation is temporary.

Flutter categorizes state based on where it’s needed:

  • Ephemeral State (Local State): State confined to a single widget or a small part of the widget tree. Examples include the current page in a PageView, an animation’s progress, or the selected tab in a TabBar. Flutter’s built-in StatefulWidget and its associated State object (using setState()) are perfectly suited for managing ephemeral state.
  • App State (Shared State): State needed by multiple, potentially distant parts of the widget tree. Examples include user preferences, login information, notifications, or the shopping cart. Managing app state solely with StatefulWidget by passing callbacks and data down the tree becomes complex and inefficient quickly (‘prop drilling’).

While StatefulWidget is essential, Flutter encourages using dedicated state management approaches for app state. These solutions provide centralized, efficient, and testable ways to manage data that needs to be shared.

Popular options include (but are not limited to):

  • Provider: (Recommended starting point) Built on top of InheritedWidget, provider simplifies dependency injection and state management. It allows exposing state objects (like ChangeNotifier or any other data) and efficiently rebuilding only the widgets that depend on that specific state when it changes.
  • Riverpod: Often considered the successor to Provider, Riverpod addresses some of Provider’s limitations, offering compile-safe state management, improved testability, and avoiding dependency on the BuildContext for accessing state.
  • Bloc/Cubit: Implements the Business Logic Component pattern, promoting separation of concerns between UI, state, and business logic. Excellent for complex applications with intricate state transitions.
  • GetX: A comprehensive framework offering state management, dependency injection, route management, and other utilities with a concise syntax.
  • MobX: Uses reactive programming principles with observables and reactions to automatically track and update state changes.

The original tutorial used scoped_model, which is now outdated and generally not recommended for new projects. Provider offers a similar conceptual simplicity but with better performance and features. This chapter will briefly demonstrate concepts using Provider where app state is involved.

Choosing the right state management solution depends on the complexity of your app and team preference. Start simple (Provider/Riverpod) and adopt more structured approaches (Bloc) as needed. Refer to the official Flutter documentation on State Management for a detailed overview: https://docs.flutter.dev/data-and-backend/state-mgmt/options

Navigation defines how users move between different screens (or ‘routes’) in your application. Flutter provides a robust routing system.

This is the traditional and often simpler way to handle navigation, suitable for many applications.

  • MaterialPageRoute: A common Route implementation that transitions screens using platform-adaptive animations. It takes a builder function that creates the widget for the new screen.
  • Navigator.push: Pushes a new route onto the navigation stack. The user sees the new screen.
  • Navigator.pop: Pops the current route from the stack, revealing the previous screen. Can optionally return a result to the previous route.
// Navigate to a new screen (ProductDetailPage)
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailPage(product: selectedProduct),
),
);
// Go back to the previous screen
Navigator.pop(context);
// Go back and return a result
Navigator.pop(context, 'Data from detail page');

For better organization, especially in larger apps, you can define named routes in your MaterialApp and navigate using Navigator.pushNamed.

// In MaterialApp:
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/productDetail': (context) => ProductDetailPage(), // Need to handle arguments separately
// ... other routes
},
);
// Navigate using the name:
Navigator.pushNamed(context, '/productDetail', arguments: selectedProduct);

Declarative Navigation (Navigator 2.0 / Router API)

Section titled “Declarative Navigation (Navigator 2.0 / Router API)”

Introduced later, Navigator 2.0 provides a more declarative and powerful API (Router, Page, RouteInformationParser, RouterDelegate). It gives finer control over the navigation stack, making it easier to handle complex scenarios like deep linking, nested routing, and browser history management (especially for web apps). However, it has a steeper learning curve than Navigator 1.0. Packages like go_router simplify working with Navigator 2.0.

Example: Simple Product Navigation (Using Navigator 1.0)

Section titled “Example: Simple Product Navigation (Using Navigator 1.0)”

Let’s structure the product list/detail example using basic navigation and StatefulWidget for local state (like the rating).

  1. Create Project: flutter create product_nav_app
  2. Add Assets: Create an assets/images/ folder, add product images (e.g., iphone.png, pixel.png), and declare the folder in pubspec.yaml:
flutter:
uses-material-design: true
assets:
- assets/images/
  1. Define Product Model (lib/product.dart):
class Product {
final String name;
final String description;
final int price;
final String image;
const Product({required this.name, required this.description, required this.price, required this.image});
// Sample data generation
static List<Product> getProducts() {
return [
const Product(name: "Pixel", description: "Pixel is the most feature-full phone ever", price: 800, image: "pixel.png"),
const Product(name: "Laptop", description: "Laptop is most productive development tool", price: 2000, image: "laptop.png"),
const Product(name: "Tablet", description: "Tablet is the most useful device ever for meeting", price: 1500, image: "tablet.png"),
const Product(name: "Pendrive", description: "Pendrive is useful storage medium", price: 100, image: "pendrive.png"),
const Product(name: "Floppy Drive", description: "Floppy drive is useful rescue storage medium", price: 20, image: "floppy.png"),
const Product(name: "iPhone", description: "iPhone is the stylish phone ever", price: 1000, image: "iphone.png"),
];
}
}
  1. Create RatingBox Widget (lib/rating_box.dart): (Manages its own ephemeral state)
import 'package:flutter/material.dart';
class RatingBox extends StatefulWidget {
const RatingBox({Key? key}) : super(key: key);
@override
_RatingBoxState createState() => _RatingBoxState();
}
class _RatingBoxState extends State<RatingBox> {
int _rating = 0;
void _setRating(int newRating) {
setState(() {
_rating = newRating;
});
}
@override
Widget build(BuildContext context) {
const double size = 20;
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
IconButton(
icon: Icon(_rating >= 1 ? Icons.star : Icons.star_border, size: size),
color: Colors.red[500],
onPressed: () => _setRating(1),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
IconButton(
icon: Icon(_rating >= 2 ? Icons.star : Icons.star_border, size: size),
color: Colors.red[500],
onPressed: () => _setRating(2),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
IconButton(
icon: Icon(_rating >= 3 ? Icons.star : Icons.star_border, size: size),
color: Colors.red[500],
onPressed: () => _setRating(3),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
);
}
}
  1. Create ProductBox Widget (lib/product_box.dart): (Displays a single product)
import 'package:flutter/material.dart';
import 'product.dart';
import 'rating_box.dart';
class ProductBox extends StatelessWidget {
final Product item;
const ProductBox({Key? key, required this.item}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(2),
height: 140,
child: Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Image.asset("assets/images/" + item.image, width: 100), // Added width for consistency
Expanded(
child: Container(
padding: const EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start, // Align text left
children: <Widget>[
Text(item.name, style: const TextStyle(fontWeight: FontWeight.bold)),
Text(item.description),
Text("Price: ${item.price.toString()}"),
const RatingBox(),
],
),
),
)
],
),
),
);
}
}
  1. Create ProductPage Widget (lib/product_page.dart): (Detail screen)
import 'package:flutter/material.dart';
import 'product.dart';
import 'rating_box.dart';
class ProductPage extends StatelessWidget {
final Product item;
const ProductPage({Key? key, required this.item}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(item.name),
),
body: Center(
child: Container(
padding: const EdgeInsets.all(16), // Added padding
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center, // Center content
children: <Widget>[
Image.asset("assets/images/" + item.image, height: 200), // Adjusted size
const SizedBox(height: 20), // Added spacing
Text(item.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24)),
const SizedBox(height: 10),
Text(item.description, textAlign: TextAlign.center),
const SizedBox(height: 10),
Text("Price: ${item.price.toString()}", style: const TextStyle(fontSize: 20)),
const SizedBox(height: 10),
const RatingBox(),
const Spacer(), // Push button to bottom
ElevatedButton( // Modern button
onPressed: () {
Navigator.pop(context);
},
child: const Text('Go Back'),
)
],
),
),
),
);
}
}
  1. Update main.dart: (Main app structure and home screen list)
import 'package:flutter/material.dart';
import 'product.dart';
import 'product_box.dart';
import 'product_page.dart';
// Assuming rating_box.dart is also created
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Navigation Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const MyHomePage(title: 'Product List'),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
final List<Product> items = Product.getProducts();
const MyHomePage({Key? key, required this.title}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return GestureDetector(
child: ProductBox(item: items[index]),
onTap: () {
// Navigate to ProductPage when tapped
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductPage(item: items[index]),
),
);
},
);
},
),
);
}
}

Run the application. You’ll see a list of products. Tapping a product navigates to its detail page. The rating stars on both pages maintain their own local state independently using StatefulWidget (RatingBox). App state (like a global shopping cart) would require a solution like Provider.