Flutter StateProvider

StateProvider exposes ways to change its state.

It is a simplified version of StateNotifierProvider, Designed to avoid writing StateNotifier classes for very simple use cases.

StateProvider exists primarily to allow changes to simple variables through the UI. The state of the StateProvider is usually: enums, such as filter types a string, usually the raw content of a filebox (TextField or something) logical variable, for checkboxes Number, for age, in pagination or form.

StateProvider should not be used in the following situations:

State requires validation logic States are complex objects (e.g. custom classes, lists/Maps, etc.) The logic for changing state is much more advanced than simple count++ .

For more advanced scenarios, consider using StateNotifierProvider instead, and create a StateNotifier class. For long-term project maintainability, custom StateNotifier classes are dangerous when the initial boilerplate will be large - because it centralizes the business logic of the state in a single place.

Usage example: use the drop-down box to change the filter type

A real use case for StateProvider would be to manage simple form components like dropdown/text/checkbox. In particular, we'll see how to use a StateProvider to implement a dropdown box that allows changing how the item list is sorted.

In order to simplify processing, the product list will be built directly in the application as follows

class Product {
  Product({required this.name, required this.price});

  final String name;
  final double price;
}

final _products = [
  Product(name: 'iPhone', price: 999),
  Product(name: 'cookie', price: 2),
  Product(name: 'ps5', price: 500),
];

final productsProvider = Provider<List<Product>>((ref) {
  return _products;
});

In order to simplify processing, the product list will be built directly in the application as follows

In a real application, this list would usually be fetched via a network request using a FutureProvider.

The UI will then display the list of items as follows:

Widget build(BuildContext context, WidgetRef ref) {
  final products = ref.watch(productsProvider);
  return Scaffold(
    body: ListView.builder(
      itemCount: products.length,
      itemBuilder: (context, index) {
        final product = products[index];
        return ListTile(
          title: Text(product.name),
          subtitle: Text('${product.price} $'),
        );
      },
    ),
  );
}

Now that we have the basics done, we can add dropdown boxes that allow filtering of items by price or by name. To achieve this, we will use DropDownButton.

enum ProductSortType {
  name,
  price,
}

Widget build(BuildContext context, WidgetRef ref) {
  final products = ref.watch(productsProvider);
  return Scaffold(
    appBar: AppBar(
      title: const Text('Products'),
      actions: [
        DropdownButton<ProductSortType>(
          value: ProductSortType.price,
          onChanged: (value) {},
          items: const [
            DropdownMenuItem(
              value: ProductSortType.name,
              child: Icon(Icons.sort_by_alpha),
            ),
            DropdownMenuItem(
              value: ProductSortType.price,
              child: Icon(Icons.sort),
            ),
          ],
        ),
      ],
    ),
    body: ListView.builder(
      // ... 
    ),
  );
}

Now that we have the dropdown, let's create a StateProvider and use the provider to synchronize the state of the dropdown.

First, create a StateProvider :

final productSortTypeProvider = StateProvider<ProductSortType>(
  (ref) => ProductSortType.name,
);

Then, we can connect the provider to the dropdown as follows:

DropdownButton<ProductSortType>(
  value: ref.watch(productSortTypeProvider),
  onChanged: (value) =>
      ref.read(productSortTypeProvider.notifier).state = value!,
  items: [
    // ...
  ],
),

With that, we should now be able to change the sort type. Although it doesn't affect the item list yet!

Now for the last part: updating the productsProvider to sort the list of products.

The key component to achieve this is to use ref.watch to have the productsProvider get the sort type and recompute the product list whenever the sort type changes.

The implementation would be:

final productsProvider = Provider<List<Product>>((ref) {
  final sortType = ref.watch(productSortTypeProvider);
  switch (sortType) {
    case ProductSortType.name:
      return _products.sorted((a, b) => a.name.compareTo(b.name));
    case ProductSortType.price:
      return _products.sorted((a, b) => a.price.compareTo(b.price));
  }
});

That's all! It's enough for the UI to automatically re-render the item list when the sort type changes.

Full example on Dartpad:

import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

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

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

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

class Product {
  Product({required this.name, required this.price});

  final String name;
  final double price;
}

final _products = [
  Product(name: 'iPhone', price: 999),
  Product(name: 'cookie', price: 2),
  Product(name: 'ps5', price: 500),
];

enum ProductSortType {
  name,
  price,
}

final productSortTypeProvider = StateProvider<ProductSortType>(
  // 我们返回排序类型的默认值,这里是名称。
  (ref) => ProductSortType.name,
);

final productsProvider = Provider<List<Product>>((ref) {
  final sortType = ref.watch(productSortTypeProvider);
  switch (sortType) {
    case ProductSortType.name:
      return _products.sorted((a, b) => a.name.compareTo(b.name));
    case ProductSortType.price:
      return _products.sorted((a, b) => a.price.compareTo(b.price));
  }
});

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final products = ref.watch(productsProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Products'),
        actions: [
          DropdownButton<ProductSortType>(
            // 当排序类型改变时,这会重新构建下拉框来改变显示的图标。
            value: ref.watch(productSortTypeProvider),
            // 当用户和下拉框交互时,我们更新 provider 的状态。
            onChanged: (value) =>
                ref.read(productSortTypeProvider.notifier).state = value!,
            items: const [
              DropdownMenuItem(
                value: ProductSortType.name,
                child: Icon(Icons.sort_by_alpha),
              ),
              DropdownMenuItem(
                value: ProductSortType.price,
                child: Icon(Icons.sort),
              ),
            ],
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: products.length,
        itemBuilder: (context, index) {
          final product = products[index];
          return ListTile(
            title: Text(product.name),
            subtitle: Text('${product.price} \$'),
          );
        },
      ),
    );
  }
}

How to avoid reading provider twice when updating state based on previous value

final counterProvider = StateProvider<int>((ref) => 0);

class HomeView extends ConsumerWidget {
  const HomeView({Key? key}): super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // it will read the value twice
          ref.read(counterProvider.notifier).state = ref.read(counterProvider.notifier).state + 1;
        },
      ),
    );
  }
}

There's nothing particularly wrong with this version of the code either, just a little syntactically inconvenient.

To make the syntax look nicer, use the update function. This function will receive a callback function that will receive the current state and expect the new state to be returned as a result.

We can use it to refactor the previous code:

final counterProvider = StateProvider<int>((ref) => 0);

class HomeView extends ConsumerWidget {
  const HomeView({Key? key}): super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(counterProvider.notifier).update((state) => state + 1);
        },
      ),
    );
  }
}

 

Another example

import 'package:flutter_riverpod/flutter_riverpod.dart';

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

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

  @override
  Widget build(BuildContext context) {

    return  MaterialApp(

        home: Scaffold(
          appBar: AppBar(
            title: Text("Riverpod App"),
          ),
          body: SelectedButton(),
        ),
      );

  }
}
final selectedButtonProvider = StateProvider<String>((ref) => '');
// 2
final isRedProvider = Provider<bool>((ref) {
  final color = ref.watch(selectedButtonProvider);
  return color == 'red'; // true if red
});

class SelectedButton extends ConsumerWidget {
  const SelectedButton({ Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 3
    final selectedButton = ref.watch(selectedButtonProvider);
    // 4
    final isRed = ref.watch(isRedProvider);

    return Center(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(selectedButton),

          ElevatedButton(
            onPressed: () => ref.read(selectedButtonProvider.notifier).state = 'red',
            child: Text('Red'),
          ),
          ElevatedButton(
            // 5
            onPressed: () => ref.read(selectedButtonProvider.notifier).state = 'blue',
            child: Text('Blue'),
          ),
          // 6
          isRed ? Text('Color is red') : Text('Color is not red')
        ],
      ),
    );
  }
}

Recent posts