【Flutter】親Widgetから子のメソッドを呼ぶには、GlobalObjectKeyを使う

## はじめに

こんにちは😀 **karamage** です。
Flutterでアプリを作るのが大好きなアプリエンジニアです。

この投稿は、[Flutter Advent Calendar2021](https://qiita.com/advent-calendar/2021/flutter) **11日目** の記事です。

この記事では、**親Widgetから子のメソッドを呼ぶ方法**について、書いています。

この記事のサンプルコードについて、以下のリポジトリに上げておきます。
https://github.com/karamage/flutter_global_object_key_example

## 親から子のメソッドを呼ぶには?

Flutterで、**親Widgetから子Widgetのメソッド(関数)を呼び出したり、子の状態を参照したい** 場合ってありませんか?

ぼくは、あります。

結論から言うと、親から子のメソッドを呼ぶには、**GlobalObjectKey** を使います。

`GlobalObjectKey`は、親Widget側で使われる機能で、**Keyを設定した子Widget(Stateful)の状態変化やプロパティやメソッドを親から参照できる**ようになります。

このとき、**どのようにして呼び出したらよいのか、使い方をよく忘れてしまう**ので、自分の備忘録のためにも、ここに書き記しておこうと思います。

この記事が、誰かのお役に立てれば幸いです。

## GlobalObjectKeyを使って、親から子のメソッドを呼んでみる

**GlobalObjectKeyとは何か**、という**小難しい説明はいったん置いといて**、実際の使い方について、例を挙げて説明します。

以下のような、**よくあるカウンターアプリ**を作るとします。

![Screen Shot 2021-12-05 at 15.23.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93703/5c863c74-e3be-3c50-9db6-55c09ff077b8.png)

### 子Widget(Stateful)の定義

内部にStatefulなcounterの状態を持ち、表示を行うWidgetを、`Counter`クラスとして定義します。
CounterクラスはStateful Widgetで、`CounterState`を持ちます。
`CounterState`には、counterを1増やす`incrementCounter`というメソッドを持っています。

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

class Counter extends StatefulWidget {
  Counter({Key? key}) : super(key: key);

  @override
  CounterState createState() => CounterState();
}

class CounterState extends State<Counter> {
  int _counter = 0;

  void incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Text(
      '$_counter',
      style: Theme.of(context).textTheme.headline4,
    );
  }
}
```

## 親Widget(Stateless)の定義

続いて、CounterをWrapする親Widgetとして、以下のような`CounterPage`クラスを定義します。

```diff_dart:counter_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_global_object_key_example/counter.dart';

class CounterPage extends StatelessWidget {
  final String title;
  CounterPage({Key? key, required this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Counter(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
+         // TODO ここでCounterのincrementCounter()を呼び出したい
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

```

さて、ここで問題になるのが、**FABを押した際に`Counter`の`incrementCounter()`を呼び出したい**のですが、どのようにすればよいのでしょうか？

答えは、以下のように**GlobalObjectKeyを使って呼び出す**ことができます。

```diff_dart:counter_page.dart
  @override
  Widget build(BuildContext context) {
+   final counterKey = GlobalObjectKey<CounterState>(context);
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
+       child: Counter(key: counterKey),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
+         // ここでCounterのincrementCounter()を呼び出す
+         counterKey.currentState?.incrementCounter();
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
```

counterKeyを定義して、Counterのコンストラクタに渡して登録しています。
`counterKey.currentState`を参照することによって、現在の`CounterState`の状態にアクセスすることができます。
GlobalObjectKeyは、**VueやReactでいうと、ref参照みたいなもの**だと理解しています。

## おまけ：親Widgetから子Widgetのstateを直接参照して書き換えたい

まれにですが、子のメソッド呼び出しではなくて、**子のstateを直接参照**して書き換えたいこともあるかと思います。

その際は、以下のようにして、stateを参照します。

```diff_dart:counter_page.dart
class CounterPage extends StatelessWidget {
  final String title;
  CounterPage({Key? key, required this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final counterKey = GlobalObjectKey<CounterState>(context);
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Counter(key: counterKey),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
+         final count = counterKey.currentState?.counter ?? 0;
+         counterKey.currentState?.setCounter(count + 1);
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
```

`Counter`は以下のように書き換えてください。
**状態を外からアクセス可能**にするため、privateからpublicにするのに`_counter`の`_`アンスコを外しています。

```diff_dart:counter.dart
class CounterState extends State<Counter> {
- int _counter = 0;
+ int counter = 0;

+ void setCounter(int value) {
+   setState(() {
+     counter = value;
+   });
+ }

  @override
  Widget build(BuildContext context) {
    return Text(
      '$counter',
      style: Theme.of(context).textTheme.headline4,
    );
  }
}
```

## おまけ2：子から親のメソッドを呼びたい

逆に、**子から親のメソッドを呼びたい**場合もあるかと思います。
このときは、**親から子にコールバック関数を渡してください。**
子は必要なときに、親から渡されたコールバック関数を呼んでください。

```diff_dart
class Counter extends StatefulWidget {
+ Function callback;
+ Counter({Key? key, required this.callback}) : super(key: key);

  @override
  CounterState createState() => CounterState();
}
```

## まとめ

**親Widgetから子のメソッドを呼ぶには、GlobalObjectKeyを使いましょう！**

GlobalObjectKeyとBuildContextを組み合わせることで、StatelessWidget内でGlobalKeyを使用することができます。これにより、グローバル変数の使用を避けることができ、子Widgetの再利用が可能になり、メソッド呼び出しや状態参照ができます。

ところで、そもそも、**GlobalObjectKey(GlobalKey)って、いったいなんぞや！？**という疑問をお持ちの方、いると思います。

GlobalObjectKeyや、**GlobalKeyとの違い**や、**他のKey**について、詳しく知りたい場合、以下の記事やツイートが参考になります。
(**実は、ぼくも理解が不足している部分もある**ので、勉強し直します…うごごご)

**Flutter Widget Keyの種類と使い方について**

https://qiita.com/kurun_pan/items/0517fb62f1b47c90882c

最後まで読んでいただいてありがとうございます！
それでは、みなさま、よい年末を！

## 参考

Remiさんのツイート
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Random <a href="https://twitter.com/hashtag/Flutter?src=hash&amp;ref_src=twsrc%5Etfw">#Flutter</a> tip:<br><br>You can use GlobalKeys inside StatelessWidget by combining GlobalObjectKey and BuildContext (and potentially tuples)<br><br>This avoids using global variables, which therefore makes the widget reusable. 🙂 <a href="https://t.co/uWs9x8fuIS">pic.twitter.com/uWs9x8fuIS</a></p>&mdash; Remi Rousselet (@remi_rousselet) <a href="https://twitter.com/remi_rousselet/status/1217832292334346241?ref_src=twsrc%5Etfw">January 16, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

@mono0926 さんのツイート
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">なぜGlobalKey()ではなくGlobalObjectKey(context)にする必要があるかを示すサンプルを書いた(　´･‿･｀)<a href="https://t.co/4knKlGo6HM">https://t.co/4knKlGo6HM</a> <a href="https://t.co/MVrkA9gvoU">https://t.co/MVrkA9gvoU</a> <a href="https://t.co/eTMRiC6k9r">pic.twitter.com/eTMRiC6k9r</a></p>&mdash; mono (@_mono) <a href="https://twitter.com/_mono/status/1217940729839808512?ref_src=twsrc%5Etfw">January 16, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>