Flutter State Management Made Simple: From Local to Global with Provider

Maxim Gorin
5 min readSep 19, 2024

--

In the previous article of our beginner-friendly Flutter series, Handling Multi-Screen Navigation in Flutter: A Practical Tutorial, we explored how to navigate between screens and pass data in Flutter. Now, we’re diving into something just as crucial — state management.

This series is designed specifically for beginners, and all examples are crafted to run seamlessly in DartPad. Why DartPad? Because it allows you to jump straight into coding without the hassle of setting up an IDE, making it an excellent tool for learning the fundamentals of Flutter quickly and efficiently.

Learn Flutter

State management might sound intimidating, but it’s the backbone of creating dynamic, interactive apps. Whether you’re keeping track of a shopping cart, user preferences, or just a simple counter, understanding how to manage state effectively is essential. This article will break down the concept of state management in Flutter, show you the difference between local and global state, and guide you through managing global state using Provider. By the end, you'll have a working example that you can build and run directly in DartPad.

Understanding Local and Global State

Before we dive into the code, let’s clarify what we mean by “state.”

In Flutter, state refers to any data that can change and affect what is displayed on the screen. For example, the text in a TextField, the current value of a counter, or whether a checkbox is checked—all of these are examples of state.

Local State

Local state is tied to a specific widget and is typically managed within that widget. It’s suitable for simple cases where the state doesn’t need to be shared across multiple widgets or screens. For example, if you’re building a counter that increments when a button is pressed, this state can be local.

Here’s a simple example:

import 'package:flutter/material.dart';

void main() {
runApp(CounterApp());
}

class CounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Counter Example')),
body: CounterWidget(),
),
);
}
}

class CounterWidget extends StatefulWidget {
@override
_CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Counter: $_counter'),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment'),
),
],
),
);
}
}

Explanation: In this example, the _counter state is local to the CounterWidget. It’s managed and updated within this widget alone using setState, which tells Flutter to rebuild the widget with the new data.

Example: Local State

Global State

Global state, on the other hand, is state that needs to be accessible from multiple parts of the app. For example, if you have a shopping cart that needs to be accessed from various screens, you’ll need a way to manage that state globally.

Managing global state can become complex, especially as your app grows. This is where tools like Provider come in handy, making it easier to share and manage state across the entire app.

State Management with Provider

Provider is a package that simplifies state management by allowing you to share data across your app and rebuild UI parts when the data changes. It’s perfect for beginners because it’s both easy to use and powerful enough to handle more complex cases as your app scales.

Core Concepts

  • ChangeNotifier: This class helps notify listeners (widgets) when the state changes. It’s the core of most state management solutions in Flutter.
  • Provider: This widget is used to make an instance of ChangeNotifier available throughout the widget tree.
  • Consumer: A widget that listens to Provider and rebuilds whenever the state it depends on changes.

Creating an App with Global State Management

Let’s build an example where users can add items to a list, and this state will be managed globally using Provider. The entire example will run in DartPad.

Step 1: Setting Up the Project

If you’re working in an IDE like Android Studio or VS Code, you’ll need to set up the project and include the Provider package. However, if you're using DartPad, you can skip this step, as DartPad already supports Provider without additional setup.

For IDE users, add the dependency in pubspec.yaml:

dependencies:
flutter:
sdk: flutter
provider: ^6.0.0

Step 2: Creating the State Model

We’ll start by creating a state model that will manage the list of items.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class ItemModel extends ChangeNotifier {
List<String> _items = [];
List<String> get items => _items;

void addItem(String item) {
_items.add(item);
notifyListeners();
}

void removeItem(int index) {
_items.removeAt(index);
notifyListeners();
}
}

Explanation: In the ItemModel class:

  • _items: A private list that holds the state of our items.
  • addItem: Adds a new item to the list and notifies listeners that the state has changed.
  • removeItem: Removes an item from the list by its index and notifies listeners.

Step 3: Providing the State Model

Next, we wrap our app with ChangeNotifierProvider to provide the ItemModel to all widgets in the app.

void main() {
runApp(
ChangeNotifierProvider(
create: (context) => ItemModel(),
child: MyApp(),
),
);
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: ItemListScreen(),
);
}
}

Explanation:

  • The ChangeNotifierProvider makes the ItemModel available to the entire widget tree.
  • Any widget in the tree can now access and modify the global state.

Step 4: Building the UI

Now, let’s build the UI where users can add and remove items from the list.

class ItemListScreen extends StatelessWidget {
final TextEditingController _controller = TextEditingController();

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Item List')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _controller,
decoration: InputDecoration(labelText: 'Enter item'),
),
),
ElevatedButton(
onPressed: () {
Provider.of<ItemModel>(context, listen: false)
.addItem(_controller.text);
_controller.clear();
},
child: Text('Add Item'),
),
Expanded(
child: Consumer<ItemModel>(
builder: (context, itemModel, child) {
return ListView.builder(
itemCount: itemModel.items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(itemModel.items[index]),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () {
Provider.of<ItemModel>(context, listen: false)
.removeItem(index);
},
),
);
},
);
},
),
),
],
),
);
}
}

Explanation:

  • TextField: Captures the item to be added.
  • ElevatedButton: Triggers the addItem method in ItemModel to add the item to the list.
  • Consumer: Listens to changes in ItemModel and rebuilds the ListView whenever an item is added or removed.
Example: App with Global State Management

Conclusion

State management is a fundamental aspect of building robust Flutter applications. Understanding the difference between local and global state, and knowing how to manage global state with tools like Provider, will set you up for success as you build more complex apps. This example is just the beginning—experiment with state management, try different scenarios, and see how it transforms the way you build apps.

Don’t hesitate to share your progress or thoughts in the comments, and if you’re finding this series helpful, be sure to subscribe for more tutorials. Keep coding, and remember, every step you take makes you a better developer!

--

--

Maxim Gorin
Maxim Gorin

Written by Maxim Gorin

Team lead in mobile development with a passion for Fintech and Flutter. Sharing insights and stories from the tech and dev world on this blog.

Responses (2)