State management is one of the key areas of Flutter development, as it determines how changes in user interactions and data to be displayed are handled. Three popular state management methods are introduced in detail: setState, Provider and BLoC (Business Logic Component). The actual problem, the complexity of the application, and the maintenance requirements will determine which method is appropriate for an application.

What is state management?

A state is a piece of data that affects how a widget looks or behaves. For example, the colour of a button, the value of a counter, or the contents of a shopping list can all be associated with a state. State management is the tool that helps the application continuously monitor and manage these changes.

There are two states of state management

Temporary or local state
A temporary or local state is data that can be stored and managed within a single widget. For example, this could be
  • the current tab in a NavigationBar
  • the state of a UI animation
  • the selected value in a list
Application State
Application state is persistent data used in many parts of the application, reacts to changes, and persists between user sessions. Examples include
  • the details of the logged-in user
  • the contents of the shopping basket in a commercial application
  • the list of favourites
Master state management in Flutter app development
From a state management point of view, it is important to distinguish between two types of widgets in Flutter app development.
Stateless widget
A widget that has no state. This means the widget does not change while the application is running and does not need to be redrawn when its state changes. It is used in cases where the UI element is static and does not change, such as a static text or icon. When we use state management in a stateless widget, we need to manage that state somewhere else, such as in a parent stateful widget, or with a global state management tool (such as Provider or BLoC).
Stateful widget
A widget with its state, which when it changes, the UI should react to those changes. The system then redraws the widget based on the new state. This widget is used for local state management.

State management solutions

1. setState

setState is the simplest state management solution and should be used when state changes are simple and local. In such cases, only the widget in question is affected by the change, and the widget is redrawn when the state is updated. To do this, we need to use StatefulWidget, which has the built-in method setState.
Example:
<
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterEvent>((event, emit) {
      if (event is Increment) {
        emit(state + 1);
      } else if (event is Decrement) {
        emit(state - 1);
      }
    });
  }
}

abstract class CounterEvent {}

class Increment extends CounterEvent {}

class Decrement extends CounterEvent {}

class CounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => CounterBloc(),
      child: Column(
        children: [
          Expanded(child: CounterWidgetA()),
          Expanded(child: CounterWidgetB()),
        ],
      ),
    );
  }
}

class CounterWidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counterBloc = BlocProvider.of<CounterBloc>(context);
    return Scaffold(
      body: Center(
        child: BlocBuilder<CounterBloc, int>(
          builder: (context, count) {
            return Text('$count');
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => counterBloc.add(Increment()),
            child: Icon(Icons.add),
          ),
          FloatingActionButton(
            onPressed: () => counterBloc.add(Decrement()),
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}



class CounterWidgetB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: BlocBuilder<CounterBloc, int>(
          builder: (context, count) {
            return Text('$count');
          },
        ),
      ),
    );
  }
}
In this example, the counter value is updated by the setState function when the Add button is pressed, causing the widget to be redrawn.

2. Provider

Provider is a popular solution for applications of medium complexity where the state needs to be available across multiple widgets. Provider is used when the application has a larger structure and the state needs to be monitored by several different widgets. It supports easy state visibility and sharing.
Flutter state management: best practices & solutions
Example:
class Counter extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

class CounterProviderWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => Counter(),
      child: Column(
        children: [
          Expanded(child: CounterWidgetA()),
          Expanded(child: CounterWidgetB()),
        ],
      ),
    );
  }
}

class CounterWidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<Counter>(
      builder: (context, counter, child) {
        return Scaffold(
          body: Center(child: Text('${counter.count}')),
          floatingActionButton: FloatingActionButton(
            onPressed: counter.increment,
            child: const Icon(Icons.add),
          ),
        );
      },
    );
  }
}

class CounterWidgetB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<Counter>(
      builder: (context, counter, child) {
        return Center(child: Text('${counter.count}'));
      },
    );
  }
}

3. BLoC

One of the most commonly used state management solutions in Flutter is BLoC (Business Logic Component). BLoC helps to separate application logic and state management from the user interface, making code easier to maintain, test and scale. It does this by receiving user interactions or other external data as events and returning states to widgets. This allows the UI to focus on displaying the current state while BLoC handles the changes.
Choosing the right state management for your Flutter app
Example:
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterEvent>((event, emit) {
      if (event is Increment) {
        emit(state + 1);
      } else if (event is Decrement) {
        emit(state - 1);
      }
    });
  }
}

abstract class CounterEvent {}

class Increment extends CounterEvent {}

class Decrement extends CounterEvent {}

class CounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => CounterBloc(),
      child: Column(
        children: [
          Expanded(child: CounterWidgetA()),
          Expanded(child: CounterWidgetB()),
        ],
      ),
    );
  }
}

class CounterWidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counterBloc = BlocProvider.of<CounterBloc>(context);
    return Scaffold(
      body: Center(
        child: BlocBuilder<CounterBloc, int>(
          builder: (context, count) {
            return Text('$count');
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => counterBloc.add(Increment()),
            child: Icon(Icons.add),
          ),
          FloatingActionButton(
            onPressed: () => counterBloc.add(Decrement()),
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}



class CounterWidgetB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: BlocBuilder<CounterBloc, int>(
          builder: (context, count) {
            return Text('$count');
          },
        ),
      ),
    );
  }
}
In this solution, the Increment and Decrement events control CounterBloc, and in response to these events, the counter value is updated and displayed in two separate widgets. To provide the state here, we use BlocProvider, and thanks to BlocBuilder, only the underlying widget is redrawn in response to the change.
Advantages:
  • Code reusability: Separating logic and state management makes it easier to maintain and reuse code.
  • Testability: Because BLoC separates the business logic from the UI, each part is easier to test.
  • Consistency: stream-based data flow ensures that the state is always up-to-date and that all widgets react to changes.
Disadvantages:
  • Complexity: The learning curve of the BLoC pattern can be steep, especially for smaller projects.
  • Boilerplate code: the BLoC implementation requires a lot of boilerplate code, especially for smaller state management tasks.

When to choose a state management solution?

It makes sense to combine solutions for a single application.
Local state management is used specifically for cases where the state is specific to a widget, in which case no other component is affected by its change.
Application state solutions are useful when you want to store data that is important to multiple widgets or even the entire application. Of these, Provider is recommended for medium complexity applications where simpler data models need to be implemented to represent states.
BLoC is ideal for larger projects where transparency, ease of maintenance and scalability are important, as it sufficiently separates the business logic from the user interface. It is most effective when the application requires complex logic and state management.
At LogiNet we can help you develop both native and Flutter mobile applications. Get in touch with our colleagues and let's talk about the best solution for you!

Let's talk about

your project

Drop us a message about your digital product development project and we will get back to you within 2 days.
We'd love to hear the details about your ideas and goals, so that our experts can guide you from the first meeting.
John Radford
Client Services Director UK