🥕

overrideWithは、main関数の中以外でも呼べるらしい?

2023/11/01に公開

Overview

最近仕事で、RiverpodのoverrideWithのコードをmain関数以外のところで呼ばれているのを見ることがあった...

どうやら本当にできるみたいだ👇
https://riverpod.dev/ja/docs/concepts/scopes

使い方の例は私はよく知らないのですが、Providerを上書きするのが目的ですね。

同様に、ウィジェット ツリー内の任意の場所に他の ProviderScope を挿入して、アプリケーションの一部のみのプロバイダーの動作をオーバーライドすることができます。

final themeProvider = Provider((ref) => MyTheme.light());

void main() {
  runApp(
    ProviderScope(
      child: MaterialApp(
        // Home uses the default behavior for all providers.
        home: Home(),
        routes: {
          // Overrides themeProvider for the /gallery route only
          '/gallery': (_) => ProviderScope(
            overrides: [
              themeProvider.overrideWithValue(MyTheme.dark()),
            ],
          ),
        },
      ),
    ),
  );
}

summary

色々試してみた。まずは、文字を上書きしてみよう。

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

final helloProvider = Provider<String>((ref) {
  return 'Hello World';
});

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

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

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: HomePage(),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Override Example'),
      ),
      body: Center(
        child: ProviderScope(
          overrides: [
            helloProvider.overrideWith((ref) => 'Providerを上書きした😁'),
          ],
          child: Consumer(
            builder: (context, ref, child) {
              final message = ref.watch(helloProvider);
              return Text(message);
            },
          ),
        ),
      ),
    );
  }
}

ProviderScopeのparentでも上書きができるらしい?

この [ProviderScope] の子孫となる親 [ProviderContainer] を明示的にオーバーライドします。
一般的な使用例は、モーダルがスコープ指定されたプロバイダーにアクセスできるようにすることです。これ以外の場合、モーダルはウィジェット ツリーの別のブランチにあるためアクセスできません。
これは次のようにして実現できます。

ElevatedButton(
  onTap: () {
    final container = ProviderScope.containerOf(context);
    showDialog(
      context: context,
      builder: (context) {
        return ProviderScope(parent: container, child: MyModal());
      },
    );
  },
  child: Text('show modal'),
)
The [parent] variable must never change.

公式のソースコードを少し書き換えて、ダイアログとカウンターを組み合わせたプロバイダーを上書きするサンプルを作ってみた。

// Have a counter that is being incremented by the FloatingActionButton
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class CounterNotifier extends Notifier<int> {
  
  build() {
    return 0;
  }

  void increment() {
    state++;
  }
}

final counterProvider =
    NotifierProvider<CounterNotifier, int>(CounterNotifier.new);

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

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    return const MaterialApp(
      home: Home(),
    );
  }
}

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    // ボタンを押すとカウントが表示されるダイアログを表示したい
    return Scaffold(
        appBar: AppBar(
          title: const Text('ProviderScope'),
        ),
        body: Column(
          children: [
            ElevatedButton(
              onPressed: () {
                showDialog<void>(
                  context: context,
                  builder: (c) {
                    // ダイアログを ProviderScope ウィジェットでラップし、
                    // ダイアログが同じプロバイダーにアクセスできるようにするための親コンテナー
                    // ホーム ウィジェットからアクセスできます。
                    return ProviderScope(
                      // parentプロパティを使用して、上書きをすることもできる。
                      parent: ProviderScope.containerOf(context),
                      child: const AlertDialog(
                        content: CounterDisplay(),
                      ),
                    );
                  },
                );
              },
              child: const Text('Show Dialog'),
            ),
          ],
        ),
        floatingActionButton: FloatingActionButton(
          child: const Icon(Icons.add),
          onPressed: () {
            ref.read(counterProvider.notifier).increment();
          },
        ));
  }
}

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Text('$count');
  }
}

thoughts

使ってみてわかったことは、プロバイダーを上書きしているだけ。勘違いしていたのは、main関数以外の場所でも呼ぶことができるということ!
今の所上書きしないと困ったのは、Isarを使った時くらいいですね。
コンストラクターの引数を上書きするのに、使ってましたね。

hooks_riverpodを使用したサンプル
https://github.com/sakurakotubaki/IsarHooksRiverpod

これが、Isarで使った上書きをした例です

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:isar_hooks/domain/person.dart';
import 'package:isar_hooks/screen/add_page.dart';
import 'package:isar_hooks/utils/db_service.dart';
import 'package:path_provider/path_provider.dart';

void main() async {
  // Isarの初期化
  WidgetsFlutterBinding.ensureInitialized();
  // アプリのドキュメントディレクトリを取得
  final dir = await getApplicationDocumentsDirectory();
  final isar = await Isar.open(
    [PersonSchema],
    directory: dir.path,
  );

  runApp(
    ProviderScope(
      // overrides:は、Providerの値を上書きするためのものです。
      // overrideWithValueは、上書きする値を指定するためのものです。
      overrides: [
        isarProvider.overrideWithValue(isar),
      ],
      child: MyApp(isar: isar),
    ),
  );
}


class MyApp extends StatelessWidget {
  final Isar isar;

  MyApp({required this.isar});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Memo App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: AddPage(isar: isar),
    );
  }

Discussion