生産性のない時間 is プライスレス

私はFlutterのProviderを全てグローバルスコープで使っていました。(懺悔)

公開日時:

出オチです。

タイトルの通り、FlutterのProviderの取り扱いについて圧倒的勘違いをやらかしていたのでここに納めたいと思います。 いや本当に全Flutterエンジニアに土下座しないといけないかもしれない。

というのもFlutterの記事系でProviderの使い方は調べてたのですがProviderのスコープの概念がなぜか認識できてなかったというのがあります。 (自明すぎてわざわざ詳細に言及する必要もなかったのでしょう。) だってこれFlutterというよりモバイルアプリ全体にかかる状態管理の考え方ですからね。多分。

事の始まり

事の明かし

よく考えれば当然です。すべてをグローバルスコープで管理するという思想はそれはそれでありですが、 当然スコープを制限する方法は用意されててしかるべきです。というかスコープの概念はそりゃ当然あるはずです。 なぜその思考に行きついてなかったのか。(血涙

というわけで以下のようなコードを書きました。

プログラム
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [ChangeNotifierProvider(create: (_) => GlobalCounter())],
      child: const MyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Provider Scope Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Flutter Provider Scope Demo'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(title),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Text('Global Provider (全体で共有):'),
          GlobalCounterWidget(),
          const Divider(),
          const Text('ローカルProvider(このWidget配下のみ):'),
          LocalProviderScope(),
        ],
      ),
    );
  }
}

// グローバルProvider
class GlobalCounter extends ChangeNotifier {
  int value = 0;
  void increment() {
    value++;
    notifyListeners();
  }
}

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

  @override
  Widget build(BuildContext context) {
    // LocalCounterが上位のコンテキストで定義されてないので当然エラーになります。
    final counter = context.watch<LocalCounter>();
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(
          '値: ${counter.value}',
          style: Theme.of(context).textTheme.headlineMedium,
        ),
        IconButton(icon: const Icon(Icons.add), onPressed: counter.increment),
      ],
    );
  }
}

// ローカルProviderのスコープ例
class LocalCounter extends ChangeNotifier {
  int value = 100;
  void increment() {
    value++;
    notifyListeners();
  }
}

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => LocalCounter(),
      child: LocalCounterWidget(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final counter = context.watch<LocalCounter>();
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(
          '値: ${counter.value}',
          style: Theme.of(context).textTheme.headlineMedium,
        ),
        IconButton(icon: const Icon(Icons.add), onPressed: counter.increment),
      ],
    );
  }
}

図にするとこんな感じになるはず?

スコープ図

記載されている通り、このようにLocalCounterが定義されていないスコープでLocalCounterを呼び出すと、グローバルのMultiProviderではLocalCounterは定義されていないので 当然のように実行時エラーになります。Providerにはこのスコープの概念が当然にあります。

後日談。というか今回のオチ。

モバイルアプリのこういう自明な概念について解説されている教科書知りませんか!!!

だれか、僕を許してくれ……。