State Management in Flutter: Provider, Riverpod, and BLoC

Aug 11, 2023
content_copy

In this article, we will provide a general overview of state management and then delve into some of the most interesting state management solutions, that includes Provider, Riverpod, and BLoC

State management is a complex topic.

As you explore Flutter, there comes a time when you need to share application state between screens throughout your app. There are numerous approaches you can consider.

Points to consider before choosing an approach.

Provider

By using provider instead of manually writing InheritedWidget, you get:

  • simplified allocation/disposal of resources
  • lazy-loading
  • a vastly reduced boilerplate over making a new class every time
  • devtool friendly – using Provider, the state of your application will be visible in the Flutter devtool

You can execute the following command to install Provider:

flutter pub add provider

When using Provider, you need to comprehend three concepts: ChangeNotifier, ChangeNotifierProvider, and Consumer.

ChangeNotifier

ChangeNotifier is a straightforward class provided by the Flutter SDK that offers change notifications to its listeners. In simpler terms, if something is a ChangeNotifier, you can subscribe to receive notifications about its changes.

In Provider, ChangeNotifier is a means to encapsulate your application state. For uncomplicated apps, a single ChangeNotifier may suffice. However, in more complex applications, you may have multiple models, resulting in several ChangeNotifiers.

The only code specific to ChangeNotifier is the invocation of notifyListeners(). Employ this method whenever the model undergoes changes that could impact your app’s UI. All other elements in CartModel pertain to the model itself and its associated business logic.

Here’s an example of our DataProvider:


class Counter with ChangeNotifier, DiagnosticableTreeMixin {
  int _count = 0;

  int get count => _count;

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

  /// Makes `Counter` readable inside the devtools by listing all of its properties
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(IntProperty('count', count));
  }
}

ChangeNotifierProvider

ChangeNotifierProvider is a widget that furnishes its descendants with an instance of a ChangeNotifier. This widget is part of the provider package.

Placing ChangeNotifierProvider is straightforward; it should be positioned above the widgets requiring access to it.

In the given example, we have a ProviderPage containing a ChangeNotifierProvider


void main() {
  runApp(
    /// Providers are above [MyApp] instead of inside it, so that tests
    /// can use [MyApp] while mocking the providers
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => Counter()),
      ],
      child: const MyApp(),
    ),
  );
}

Consumer

A recommended practice is to position your Consumer widgets as deep in the widget tree as feasible. This approach avoids the need to rebuild substantial segments of the UI due to minor changes in specific details.


class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Example'),
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.center,
          children: const <Widget>[
            Text('You have pushed the button this many times:'),

            /// Extracted as a separate widget for performance optimization.
            /// As a separate widget, it will rebuild independently from [MyHomePage].
            ///
            /// This is totally optional (and rarely needed).
            /// Similarly, we could also use [Consumer] or [Selector].
            Count(),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        key: const Key('increment_floatingActionButton'),

        /// Calls `context.read` instead of `context.watch` so that it does not rebuild
        /// when [Counter] changes.
        onPressed: () => context.read<Counter>().increment(),
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

class Count extends StatelessWidget {
  const Count({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(
      /// Calls `context.watch` to make [Count] rebuild when [Counter] changes.
      '${context.watch<Counter>().count}',
      key: const Key('counterState'),
      style: Theme.of(context).textTheme.headlineMedium,
    );
  }
}

Riverpod

Riverpod works in a similar fashion to Provider. It offers compile safety and testing without depending on the Flutter SDK.

You can execute the following command to install Riverpod:

flutter pub add flutter_riverpod
  • Riverpod offers a simpler and more intuitive approach to state management compared to some other solutions. It promotes clean and readable code, making it easier for developers to understand and maintain their apps.
  • Riverpod eliminates the need for a BuildContext in providers, reducing boilerplate code and making it easier to manage state across different parts of your app.
  • Riverpod encourages the use of immutable state objects, which helps prevent accidental state mutations and ensures consistent and reliable app behaviour.
  • With Riverpod, you can easily override providers’ values for testing or different scenarios, making it more flexible to handle various use cases
  • Riverpod’s support for Scoped Providers makes it convenient to manage state on a per-widget basis, minimising the scope of the state to where it’s actually needed.

Providers are the most important part of a Riverpod application. A provider is an object that encapsulates a piece of state and allows listening to that state.


final counterProvider = StateNotifierProvider((ref) {
  return Counter();
});

class Counter extends StateNotifier<int> {
  Counter(): super(0);

  void increment() => state++;
}

We need to encapsulate the entire application within a “ProviderScope” widget. This widget serves as the container for our providers’ state.


void main() {
  runApp(
    // Adding ProviderScope enables Riverpod for the entire project
    const ProviderScope(child: MyApp()),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: Home());
  }
}

class Home extends ConsumerWidget {
  const Home({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter example')),
      body: Center(
        child: Text('${ref.watch(counterProvider)}'),
      ),
      floatingActionButton: FloatingActionButton(
        // The read method is a utility to read a provider without listening to it
        onPressed: () => ref.read(counterProvider.notifier).increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}

Using ref to interact with providers

There are three primary usages for “ref”:

  • Obtaining the value of a provider and listening to changes, such that when this value changes, this will rebuild the widget or provider that subscribed to the value. This is done using ref.watch
  • Adding a listener on a provider, to execute an action such as navigating to a new page or showing a modal whenever that provider changes. This is done using ref.listen.
  • Obtaining the value of a provider while ignoring changes. This is useful when we need the value of a provider in an event such as “on click”. This is done using ref.read.

BLoC

Using the bloc library allows us to separate our application into three layers:

  • Presentation
  • Business Logic
  • Data

    • Repository
    • Data Provider

You can execute the following command to install BLoC:

flutter pub add flutter_bloc
  • BLoC enforces a clear separation between your app’s UI and business logic, making your codebase more organised and maintainable.
  • BLoC facilitates effective state management by centralising the management of your app’s state in a single location, making it easier to track and control changes.
  • BLoC scales well with larger and more complex applications, offering a structured approach that supports app growth.
  • BLoC fosters decoupling of UI components from the underlying data and logic, allowing for easier changes and updates without impacting the entire app.
  • BLoC works well with Flutter’s hot reload feature, allowing you to quickly iterate and see the effects of your code changes in real-time.

In summary, BLoC architecture offers a structured, efficient, and scalable way to manage your app’s business logic and state, contributing to a more maintainable and robust Flutter application.

Let’s explore how to utilise BlocProvider for supplying a CounterBloc to a CounterPage, and how to respond to state changes using BlocBuilder.

counter_bloc.dart


sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
final class CounterDecrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) => emit(state + 1));
    on<CounterDecrementPressed>((event, emit) => emit(state - 1));
  }
}

main.dart


void main() => runApp(CounterApp());

class CounterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (_) => CounterBloc(),
        child: CounterPage(),
      ),
    );
  }
}

counter_page.dart


class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Counter')),
      body: BlocBuilder<CounterBloc, int>(
        builder: (context, count) {
          return Center(
            child: Text(
              '$count',
              style: TextStyle(fontSize: 24.0),
            ),
          );
        },
      ),
      floatingActionButton: Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          Padding(
            padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(
              child: Icon(Icons.add),
              onPressed: () => context.read<CounterBloc>().add(CounterIncrementPressed()),
            ),
          ),
          Padding(
            padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(
              child: Icon(Icons.remove),
              onPressed: () => context.read<CounterBloc>().add(CounterDecrementPressed()),
            ),
          ),
        ],
      ),
    );
  }
}

At this point we have successfully separated our presentational layer from our business logic layer. Notice that the CounterPage widget knows nothing about what happens when a user taps the buttons. The widget simply tells the CounterBloc that the user has pressed either the increment or decrement button.

I hope this explanation / blog guides you in correct usage and implementation of State management in your Flutter App.

Leave a Reply

We welcome relevant and respectful comments. Off-topic comments may be removed.

×

Hey, having any query? Our experts are just one click away to respond you.

Contact Us
×
Always Available to help you

Connect With:

HR Sales
Whatsapp Logo
Get Quote
expand_less