# 【ProviderとRiverpodの違い①】同じ"型"を2つ以上保持できない

> Flutterにおける状態管理のためのProviderとRiverpodの違いについて、今回の記事では「同じ型のProviderを複数保持できない問題」を解決する方法について解説します。

- 公開日: 2024-10-08
- 更新日: 2024-10-15
- 著者: Matsu
- タグ: Flutter, Riverpod
- URL: https://tech.anycloud.co.jp/articles/provider-vs-riverpod-type-problem

---

## はじめに

Anycloudでは、Flutterにおける状態管理パッケージとして、どのプロジェクトでもProviderを使用しています。

「Flutterの状態管理といえばRiverpodじゃね？」と思う方も多いはず。

実際、業務委託の方にも、「Riverpod使わないんですか？」とよく聞かれます。

僕自身も、Riverpodの方が良いと思います。というか使いたいです。

でもその理由って、「今、Riverpodが主流だから」「みんなが使っているから」という浅はかな考えで止まっていました。

「Provider vs Riverpod」とかの記事をよく読んだりして、なんとなくRiverpodがいいんだろうな。

[FlutterFavorite](https://docs.flutter.dev/packages-and-plugins/favorites)になったし、そもそもProviderの開発者がRiverpodを後継として新しく開発したんだから良いに決まっている！

この程度です。

しかし、『社内でもRiverpodを推していくぞ！！！』ってなった時に、Provider以上のメリットがあることを説明できなければいけません。

ということで、今回からRiverpodについて、Providerと比較して何が良いのかを深掘りしていきます。

## Riverpodの良さ・Providerとの違いを知るためには？

<figure><img src="./image-001.webp" alt="Riverpodを使用するべき" width="1276" height="508"></figure>

Providerの違いRiverpodを知るためには、公式ドキュメントを読むのが一番早いです。

<div class="link-card-wrap"><a class="link-card" href="https://riverpod.dev/ja/docs/from_provider/provider_vs_riverpod" target="_blank" rel="noopener noreferrer"><span class="link-card-body"><span class="link-card-title">Provider vs Riverpod | Riverpod</span><span class="link-card-description">この記事では、Provider と Riverpod の違いと類似点をまとめます。</span><span class="link-card-meta"><img class="link-card-favicon" src="./linkcard-01-favicon.svg" alt=""><span class="link-card-domain">riverpod.dev</span></span></span><img class="link-card-image" src="./linkcard-01-image.webp" alt=""></a></div>

しかし、わかりにくい・・・というのもあって、

Providerユーザー向けのRiverpodというドキュメントのモチベーションページを読み解いていこうと思います。

このページでは、「**なぜ Riverpod が存在するのか？**」を詳しく説明することを目的としています。

<div class="link-card-wrap"><a class="link-card" href="https://riverpod.dev/ja/docs/from_provider/motivation" target="_blank" rel="noopener noreferrer"><span class="link-card-body"><span class="link-card-title">モチベーション | Riverpod</span><span class="link-card-description">この記事は、なぜ Riverpod が存在するのかを詳しく説明することを目的としています。</span><span class="link-card-meta"><img class="link-card-favicon" src="./linkcard-02-favicon.svg" alt=""><span class="link-card-domain">riverpod.dev</span></span></span><img class="link-card-image" src="./linkcard-02-image.webp" alt=""></a></div>

記事が何個に分かれるかわからないですが、1つずつアウトプットするつもりです。

Riverpodについてなんとなくの理解な方はこれを機に一緒に勉強しましょう。

今回の記事では、「[**Provider は同じ"型"の provider を 2 つ（またはそれ以上）保持できない**](https://riverpod.dev/ja/docs/from_provider/motivation#provider-%E3%81%AF%E5%90%8C%E3%81%98%E5%9E%8B%E3%81%AE-provider-%E3%82%92-2-%E3%81%A4%E3%81%BE%E3%81%9F%E3%81%AF%E3%81%9D%E3%82%8C%E4%BB%A5%E4%B8%8A%E4%BF%9D%E6%8C%81%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84)」というセクションを掘り下げていきます。

## Provider は同じ"型"の provider を 2 つ（またはそれ以上）保持できない

### 公式ドキュメントの説明

> 2 つの Provider<Item>を宣言すると、信頼性のない動作が発生します: InheritedWidget の API は最も近い Provider<Item>の先祖を 1 つだけ取得します。  
> Provider のドキュメントには回避策が説明されていますが、Riverpod ではこの問題は存在しません。  
> この制限を取り除くことで、以下のようにロジックを小さな部分に自由に分割できます:

```dart
@riverpod
List<Item> items(ItemsRef ref) {
return []; // ...
}

@riverpod
List<Item> evenItems(EvenItemsRef ref) {
final items = ref.watch(itemsProvider);
return [...items.whereIndexed((index, element) => index.isEven)];
```

### Providerの使用における前提

Providerユーザーにとっては、状態管理が必要な時だけにProviderを使用していると思います。

その場合、Flutterにおいてよく使われるAPIを叩く処理を使用している場合に、

-   遷移元でAPIを叩いた結果を画面遷移時に、引数に持たせてバケツリレーするか？
-   画面遷移のたびにAPIを叩いて情報を取得→表示するか？

大きくこの2つになります。

**遷移元でAPIを叩いた結果を画面遷移時に引数に持たせる場合**

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

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const FirstPage(),
    );
  }
}

class Product {
  final String name;
  final double price;

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

// APIから商品データを取得する擬似的関数
Future<List<Product>> fetchProducts() async {
  await Future.delayed(const Duration(seconds: 3));
  return [
    Product(name: "Apple", price: 1.2),
    Product(name: "Banana", price: 0.8),
    Product(name: "Cherry", price: 2.0)
  ];
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('First Page')),
      body: SafeArea(
        child: FutureBuilder<List<Product>>(
          future: fetchProducts(),
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return const Center(child: CircularProgressIndicator());
            } else if (snapshot.hasError) {
              return Text("Error: ${snapshot.error}");
            } else {
              return Column(
                children: [
                  Expanded(
                    child: ListView.builder(
                      itemCount: snapshot.data!.length,
                      itemBuilder: (context, index) {
                        var product = snapshot.data![index];
                        return ListTile(
                          title: Text(product.name),
                          subtitle: Text("\\$${product.price}"),
                        );
                      },
                    ),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      if (snapshot.data != null) {
                        Navigator.push(
                          context,
                          MaterialPageRoute(
                            builder: (context) =>
                                SecondPage(products: snapshot.data!),
                          ),
                        );
                      }
                    },
                    child: const Text('Second Page へ'),
                  ),
                ],
              );
            }
          },
        ),
      ),
    );
  }
}

class SecondPage extends StatelessWidget {
  final List<Product> products;

  const SecondPage({super.key, required this.products});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Second Page')),
      body: ListView.builder(
        itemCount: products.length,
        itemBuilder: (context, index) {
          var product = products[index];
          return ListTile(
            title: Text(product.name),
            subtitle: Text("\\$${product.price}"),
          );
        },
      ),
    );
  }
}
```

**画面遷移のたびにAPIを叩いて取得→表示する場合**

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

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const FirstPage(),
    );
  }
}

class Product {
  final String name;
  final double price;

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

// APIから商品データを取得する擬似的関数
Future<List<Product>> fetchProducts() async {
  await Future.delayed(const Duration(seconds: 3));
  return [
    Product(name: "Apple", price: 1.2),
    Product(name: "Banana", price: 0.8),
    Product(name: "Cherry", price: 2.0)
  ];
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('First Page')),
      body: SafeArea(
        child: FutureBuilder<List<Product>>(
          future: fetchProducts(),
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return const Center(child: CircularProgressIndicator());
            } else if (snapshot.hasError) {
              return Text("Error: ${snapshot.error}");
            } else {
              return Column(
                children: [
                  Expanded(
                    child: ListView.builder(
                      itemCount: snapshot.data!.length,
                      itemBuilder: (context, index) {
                        var product = snapshot.data![index];
                        return ListTile(
                          title: Text(product.name),
                          subtitle: Text("\\$${product.price}"),
                        );
                      },
                    ),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (context) => const SecondPage(),
                        ),
                      );
                    },
                    child: const Text('Second Page へ'),
                  ),
                ],
              );
            }
          },
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Second Page')),
      body: FutureBuilder<List<Product>>(
        future: fetchProducts(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return Text("Error: ${snapshot.error}");
          } else {
            return ListView.builder(
              itemCount: snapshot.data!.length,
              itemBuilder: (context, index) {
                var product = snapshot.data![index];
                return ListTile(
                  title: Text(product.name),
                  subtitle: Text("\\$${product.price}"),
                );
              },
            );
          }
        },
      ),
    );
  }
}
```

### APIからデータを取得するロジックをProviderで管理する

先ほどの例では、無駄にAPIを叩くことをしていますし、引数にしてバケツリレーするのは冗長です。

そこで、APIからデータを取得するロジックをProviderで管理することはどうでしょうか？

ロジックをアプリのどの部分からでも再利用できるようになります。

例えば、商品データを複数の画面で表示する場合、各画面で同じAPI呼び出しのコードを繰り返し書くのではなく、1つのProviderからデータを取得することができます。

これにより、コードの重複を避け、保守が容易になりますね。

また、各メソッド呼び出しで新しいインスタンスが生成されるという無駄なリソース消費も回避することができます。

### Provider は同じ"型"の provider を 2 つ（またはそれ以上）保持できない問題

さて、APIからデータを取得するロジック自体をProviderで管理することにより、アプリのどこからでも再利用できるようになります。

便利ですね！

しかし、ここで本題である「Provider は同じ"型"の provider を 2 つ（またはそれ以上）保持できない」にぶち当たります。

例えば、以下のコードを見てください。

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

void main() {
  runApp(
    MultiProvider(
      providers: [
        FutureProvider<List<Product>>(
          create: (_) => fetchProductsFromFirstSource(),
          initialData: const [],
        ),
        FutureProvider<List<Product>>(
          create: (_) => fetchProductsFromSecondSource(),
          initialData: const [],
        ),
      ],
      child: const MyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Provider App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const HomePage(),
    );
  }
}

class Product {
  final String name;
  final double price;

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

Future<List<Product>> fetchProductsFromFirstSource() async {
  return [
    Product(name: "Apple", price: 1.2),
    Product(name: "Banana", price: 0.8),
    Product(name: "Cherry", price: 2.0)
  ];
}

Future<List<Product>> fetchProductsFromSecondSource() async {
  return [
    Product(name: "Grape", price: 1.5),
    Product(name: "Orange", price: 1.5),
  ];
}

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

  @override
  Widget build(BuildContext context) {
    List<Product>? productsFromFirst = context.watch<List<Product>?>();
    List<Product>? productsFromSecond = context.watch<List<Product>?>();

    return Scaffold(
      appBar: AppBar(title: const Text('Home Page')),
      body: Column(
        children: [
          SizedBox(
            height: 300,
            child: ListView.builder(
              itemCount: productsFromFirst?.length ?? 0,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(productsFromFirst![index].name),
                  subtitle: Text("\\$${productsFromFirst[index].price}"),
                );
              },
            ),
          ),
          SizedBox(
            height: 300,
            child: ListView.builder(
              itemCount: productsFromSecond?.length ?? 0,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(productsFromSecond![index].name),
                  subtitle: Text("\\$${productsFromSecond[index].price}"),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}
```

APIのロジックをProviderで管理していくとになってくると、いずれ同じ型のProviderを作る必要が出てくるケースがあるかもしれません。

今回は意図的に同じ型を返すProviderを用意しました。

これを実際に実行してみると、`fetchProductsFromSecondSource`のデータしか取得できませんでした。

これらのProviderは同じ型を持っているため、`context.watch()` や `context.read()` を使用した場合、FlutterはProviderのリストの最後に記述されたプロバイダーを参照します。

<figure><img src="./image-002.webp" alt="同じ型のProvider実行結果" width="300" height="650"></figure>

一応、Provider のドキュメントには[回避策](https://pub.dev/packages/provider#can-i-obtain-two-different-providers-using-the-same-type)が説明されていますが、Riverpod ではそもそもこの問題は存在しません。

## Riverpodは同じ"型"の provider を2つ以上保持が可能

Riverpodの場合は、同じ型でも正常に扱うことができます。

```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({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Riverpod App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const HomePage(),
    );
  }
}

class Product {
  final String name;
  final double price;

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

final firstProductsProvider = FutureProvider<List<Product>>((ref) async {
  return fetchProductsFromFirstSource();
});

final secondProductsProvider = FutureProvider<List<Product>>((ref) async {
  return fetchProductsFromSecondSource();
});

Future<List<Product>> fetchProductsFromFirstSource() async {
  return [
    Product(name: "Apple", price: 1.2),
    Product(name: "Banana", price: 0.8),
    Product(name: "Cherry", price: 2.0)
  ];
}

Future<List<Product>> fetchProductsFromSecondSource() async {
  return [
    Product(name: "Grape", price: 1.5),
    Product(name: "Orange", price: 1.5),
  ];
}

class HomePage extends ConsumerWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final firstProducts = ref.watch(firstProductsProvider);
    final secondProducts = ref.watch(secondProductsProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Home Page')),
      body: Column(
        children: [
          SizedBox(
            height: 300,
            child: firstProducts.when(
              data: (products) => ListView.builder(
                itemCount: products.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    title: Text(products[index].name),
                    subtitle: Text("\\$${products[index].price}"),
                  );
                },
              ),
              loading: () => const CircularProgressIndicator(),
              error: (error, stack) => Text('Error: $error'),
            ),
          ),
          SizedBox(
            height: 300,
            child: secondProducts.when(
              data: (products) => ListView.builder(
                itemCount: products.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    title: Text(products[index].name),
                    subtitle: Text("\\$${products[index].price}"),
                  );
                },
              ),
              loading: () => const CircularProgressIndicator(),
              error: (error, stack) => Text('Error: $error'),
            ),
          ),
        ],
      ),
    );
  }
}
```

<figure><img src="./image-003.webp" alt="同じ型のRiverpod実行結果" width="300" height="650"></figure>

## まとめ

重要な点を再度まとめます。

-   APIのロジック自体をProvider化することによるメリット（APIを無駄に叩かない、無駄な引数をなくす）は大きい
-   Providerだと同じ"型"のprovider を2つ（またはそれ以上）保持できない（Riverpodはこの問題がない）

改めてRiverpodの良さが1つ理解できました。

次回、「providerは一度に1つの値しか吐き出さない」デュエルスタンバイ！

<div class="link-card-wrap"><a class="link-card" href="/articles/provider-vs-riverpod-one-value/" target="_blank" rel="noopener noreferrer"><span class="link-card-body"><span class="link-card-title">【ProviderとRiverpodの違い②】一度に 1 つの値しか吐き出さない</span><span class="link-card-description">Flutterにおける状態管理のためのProviderとRiverpodの違いについて、今回の記事では「一度に 1 つの値しか吐き出さない問題」を解決する方法について解説します。</span><span class="link-card-meta"><img class="link-card-favicon" src="./linkcard-03-favicon.ico" alt=""><span class="link-card-domain">tech.anycloud.co.jp</span></span></span><img class="link-card-image" src="./linkcard-03-image.webp" alt=""></a></div>
