Enable Dark Mode!
overview-of-managing-app-state-in-flutter.jpg
By: Farhana Jahan PT

Overview of Managing App State in Flutter

Technical Flutter

When building Flutter applications, one of the biggest challenges is managing state — how your app reacts when data changes. Whether it’s a counter app or a large-scale enterprise project, choosing the right state management solution can make or break your app’s scalability, performance, and maintainability.

Flutter offers many state management options, but the three most popular ones are:

  • Provider
  • Riverpod
  • BLoC (Business Logic Component)

In this blog, we’ll dive into each, compare their strengths and weaknesses, and help you decide which one suits your project best.

Provider

Provider is one of the most recommended state management solutions by the Flutter team. It’s a wrapper around InheritedWidget and makes passing data down the widget tree much easier.

It’s beginner-friendly, requires less boilerplate code, is backed by the Flutter team, and is widely used and supported. However, in large-scale apps, it can become messy and may cause unnecessary widget rebuilds if not managed carefully.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class providerPage extends StatelessWidget {
  const providerPage({super.key});
  @override
  Widget build(BuildContext context) {
    final counter = context.watch<MyProvider>().value;
    return Scaffold(
      appBar: AppBar(title: Text("Provider")),
      body: Center(
        child: Column(
          children: [
            Text("Counter$counter"),
            ElevatedButton(onPressed: () {
              context.read<MyProvider>().increment();
            }, child: Text("Counter")),
          ],
        ),
      ),
    );
  }
}
class MyProvider extends ChangeNotifier{
  int _value = 0;
  int get value => _value;
  void increment(){
    _value++;
    notifyListeners();
  }
}

This is a basic example of state management using the provider package in Flutter.

class MyProvider extends ChangeNotifier{
  int _value = 0;
  int get value => _value;
  void increment(){
    _value++;
    notifyListeners();
  }
}

This class holds the state (here, a simple counter _value).

  • _value is private, so we access it using a getter: value.
  • increment() increases _value by 1.
  • notifyListeners() tells all widgets listening to this provider to rebuild with the updated value.

This is the core of the Provider pattern: your state is in a separate class, and widgets automatically rebuild when the state changes.

class providerPage extends StatelessWidget {
  const providerPage({super.key});
  @override
  Widget build(BuildContext context) {
    final counter = context.watch<MyProvider>().value;
  • context.watch<MyProvider>() listens to changes in MyProvider.
  • When notifyListeners() is called, this widget will rebuild automatically.
  • counter holds the current value of _value.
return Scaffold(
  appBar: AppBar(title: Text("Provider")),
  body: Center(
    child: Column(
      children: [
        Text("Counter$counter"),
        ElevatedButton(onPressed: () {
          context.read<MyProvider>().increment();
        }, child: Text("Counter")),
      ],
    ),
  ),
);

  • Button calls increment() when pressed:
    • context.read<MyProvider>().increment();
      • read does not rebuild the widget. It just accesses the provider to call a function.

In your Flutter app, pressing the button calls increment(), and notifyListeners() automatically rebuilds widgets using watch, so the Text widget updates in real time to show the new counter value.

Also you must wrap your app with ChangeNotifierProvider for this to work, usually in main.dart:

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => MyProvider(),
      child: const MaterialApp(home: providerPage()),
    ),
  );
}

Without this, Flutter won’t know about the provider, and watch or read will throw an error.

Riverpod

Riverpod is a modern state management library created by the same author as Provider. It fixes many limitations of Provider and introduces compile-time safety, testability, and improved developer experience.

It provides benefits such as not requiring BuildContext, offering compile-time safety, working in pure Dart, and being highly testable and scalable, but it has a slightly steeper learning curve and a smaller, newer community compared to Provider.

import 'package:blogs_2/main.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class RiverPodPage extends ConsumerWidget{
  @override
  Widget build(BuildContext context, WidgetRef ref){
    final counter = ref.watch(counterProvider);
    return Scaffold(
      appBar: AppBar(
        title: Text("Riverpod")
      ),
      body: Center(
        child: Column(
          children: [
            Text("Counter$counter"),
            ElevatedButton(onPressed: (){
              ref.read(counterProvider.notifier).state++;
            }, child: Text("Counter"))
          ],
        ),
      ),
    );
  }
}

This is a basic counter app using Riverpod for state management.

class RiverPodPage extends ConsumerWidget{
  • Instead of a normal StatelessWidget, you use ConsumerWidget.
  • This allows you to access providers using WidgetRef ref inside the build method.
  • ref is how you read or watch providers.
final counter = ref.watch(counterProvider);
  • ref.watch(counterProvider) listens to changes in the counterProvider.
  • When the state of counterProvider changes, this widget rebuilds automatically.
  • Here, counter will hold the current value of the counter.

return Scaffold(

  appBar: AppBar(title: Text("Riverpod")),

  body: Center(

    child: Column(

      children: [

        Text("Counter$counter"),

        ElevatedButton(

          onPressed: (){

            ref.read(counterProvider.notifier).state++;

          },

          child: Text("Increment")

        )

      ],

    ),

  ),

);


When the Button is pressed, it updates the counter:

ref.read(counterProvider.notifier).state++;

Need to add the following in the main.dart file:

final counterProvider = StateProvider((ref) => 0);
  • StateProvider<int>: A provider that holds a simple piece of state of type int.
  • (ref) => 0: The initial value of the state is 0.
  • counterProvider: The name you use to access this state in your widgets using ref.watch or ref.read.

In Riverpod, the counterProvider starts with an initial value, the widget listens using ref.watch(counterProvider), pressing the button updates it via ref.read(counterProvider.notifier).state++, and since the widget watches the provider, it rebuilds automatically to display the updated counter.

Also, you must wrap your app in ProviderScope in main.dart:

void main() {
  runApp(ProviderScope(child: MyApp()));
}

BLoC

BLoC is based on Streams and Events, enforcing a clean separation between UI and business logic. It’s a very structured way to handle state, making it suitable for large enterprise-level apps.

Bloc is highly scalable, predictable, and enforces a clear separation of UI and business logic with strong community support, but it can be verbose with more boilerplate code and has a steeper learning curve compared to Provider or Riverpod.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
  }
}
class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("BLoC")),
      body: Center(
        child: Column(
          children: [
            BlocBuilder<CounterBloc, int>(
              builder: (context, count) {
                return Text(
                  "Counter: $count",
                );
              },
            ),
            ElevatedButton(onPressed: (){
              context.read<CounterBloc>().add(Increment());
            }, child: Text("Increment"))
          ],
        ),
      ),
    );
  }
}

This is a basic counter app using BloC for state management.

abstract class CounterEvent {}
class Increment extends CounterEvent {}

Events describe what can happen in the app.

  • Increment is the only event here, meaning "increase the counter".
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
  }
}

Bloc<CounterEvent, int> : this BLoC handles CounterEvents and its state is an int (the counter value).

  • super(0) : the initial state is 0.
  • on<Increment> : tells the bloc what to do when an Increment event comes in:
    • Take the current state
    • Add 1
    • Emit the new state with emit(state + 1)

So every time an Increment is dispatched, the counter increases by 1.

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("BLoC")),
      body: Center(
        child: Column(
          children: [
            BlocBuilder<CounterBloc, int>(
              builder: (context, count) {
                return Text("Counter: $count");
              },
            ),
            ElevatedButton(
              onPressed: (){
                context.read<CounterBloc>().add(Increment());
              },
              child: Text("Increment"),
            )
          ],
        ),
      ),
    );
  }
}
  • BlocBuilder<CounterBloc, int>
    • Listens to CounterBloc.
    • Whenever the bloc emits a new state (new counter value), this widget rebuilds.
    • count is the current state.
  • When the button is pressed, it dispatches an Increment event:
  • context.read().add(Increment());
  • This triggers the bloc to update state > UI rebuilds with the new counter.

You need to provide the bloc higher in the widget tree (usually in main.dart) using BlocProvider. For example:

void main() {
  runApp(
    MaterialApp(
      home: BlocProvider(
        create: (_) => CounterBloc(),
        child: CounterPage(),
      ),
    ),
  );
}

Without this, context.read<CounterBloc>() won’t find a bloc instance and will throw an error.

Provider is beginner-friendly and works well for small apps. Riverpod improves on Provider with better scalability, safety, and testability. BLoC provides a clear, structured approach ideal for large or complex applications, though it requires more boilerplate.

To read more about Overview of Navigating Routes in Flutter, refer to our blog Overview of Navigating Routes in Flutter.


If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



whatsapp_icon
location

Calicut

Cybrosys Technologies Pvt. Ltd.
Neospace, Kinfra Techno Park
Kakkancherry, Calicut
Kerala, India - 673635

location

Kochi

Cybrosys Technologies Pvt. Ltd.
1st Floor, Thapasya Building,
Infopark, Kakkanad,
Kochi, India - 682030.

location

Bangalore

Cybrosys Techno Solutions
The Estate, 8th Floor,
Dickenson Road,
Bangalore, India - 560042

Send Us A Message