web-dev-qa-db-ja.com

Flutter:継承ウィジェットを正しく使用する方法は?

InheritedWidgetを使用する正しい方法は何ですか?これまでのところ、ウィジェットツリーにデータを伝播する機会を提供することを理解しました。極端に言えば、RootWidgetであれば、すべてのルート上のツリー内のすべてのウィジェットからアクセスできます。これは、何らかの理由で、グローバルまたはシングルトンに頼らずにウィジェットのViewModel/Modelにアクセスできるようにする必要があるためです。

しかしInheritedWidgetは不変なので、どのように更新できますか?さらに重要なのは、ステートフルウィジェットがどのようにトリガーされてサブツリーが再構築されるかということです。

残念ながら、ドキュメントはここでは非常に不明瞭であり、多くの人との議論の後、誰がそれを使用する正しい方法を本当に知っているようには見えません。

ブライアン・イーガンからの引用を追加します:

はい、私はそれをツリーの下にデータを伝播する方法と考えています。 APIドキュメントから私が混乱していると思うもの:

「継承されたウィジェットは、この方法で参照されると、継承されたウィジェット自体の状態が変化したときにコンシューマーを再構築します。」

これを最初に読んだとき、私は考えました:

InheritedWidgetにいくつかのデータを詰め込み、後でそれを変更できました。その突然変異が起こると、私のInheritedWidgetを参照するすべてのウィジェットが再構築されます。

InheritedWidgetの状態を変更するには、StatefulWidgetでラップする必要があります。次に、実際にStatefulWidgetの状態を変更し、このデータをInheritedWidgetに渡します。このデータは、すべての子にデータを渡します。ただし、その場合、InheritedWidgetを参照するウィジェットだけでなく、StatefulWidgetの下のツリー全体を再構築するようです。あれは正しいですか?または、updateShouldNotifyがfalseを返す場合、InheritedWidgetを参照するウィジェットをスキップする方法をどうにか知っていますか?

64
Thomas

問題はあなたの引用から来ていますが、それは間違っています。

あなたが言ったように、InheritedWidgetsは、他のウィジェットと同様に不変です。したがって、それらはupdateしません。それらは新たに作成されます。

事柄は:InheritedWidgetは、データを保持するだけの単純なウィジェットです。更新などのロジックはありません。ただし、他のウィジェットと同様に、Elementに関連付けられています。そして何だと思う?このことは可変であり、フラッターは可能な限り再利用します!

修正された見積もりは次のようになります。

InheritedWidgetをこの方法で参照すると、InheritedElementに関連付けられたInheritedWidgetが変更されると、コンシューマーが再構築されます。

widgets/elements/renderboxがどのようにプラグインされるかについて素晴らしい話があります しかし、要するに、それらはこのようなものです(左が典型的なウィジェット、中央が「要素」、右が「レンダーボックス」です):

enter image description here

問題は、新しいウィジェットをインスタンス化するときです。 flutterはそれを古いものと比較します。 RenderBoxを指す「要素」を再利用します。そして、mutateRenderBoxプロパティ。


オーケー、しかしこれは私の質問にどのように答えますか?

InheritedWidgetをインスタンス化してからcontext.inheritedWidgetOfExactType(または基本的に同じMyClass.of)を呼び出すとき。暗示されているのは、Elementに関連付けられているInheritedWidgetをリッスンすることです。そして、そのElementは新しいウィジェットを取得するたびに、前のメソッドを呼び出したすべてのウィジェットを強制的に更新します。

つまり、既存のInheritedWidgetをまったく新しいものに置き換えると、 flutterはそれが変化したことを確認します。そして、潜在的な変更をバインドされたウィジェットに通知します。

すべてを理解していれば、すでに解決策を推測しているはずです。

InheritedWidgetStatefulWidgetでラップすると、何かが変更されたときに真新しいInheritedWidgetが作成されます。

実際のコードの最終結果は次のようになります。

class MyInherited extends StatefulWidget {
  static MyInheritedData of(BuildContext context) =>
      context.inheritFromWidgetOfExactType(MyInheritedData) as MyInheritedData;

  const MyInherited({Key key, this.child}) : super(key: key);

  final Widget child;

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

class _MyInheritedState extends State<MyInherited> {
  String myField;

  void onMyFieldChange(String newValue) {
    setState(() {
      myField = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MyInheritedData(
      myField: myField,
      onMyFieldChange: onMyFieldChange,
      child: widget.child,
    );
  }
}

class MyInheritedData extends InheritedWidget {
  final String myField;
  final ValueChanged<String> onMyFieldChange;

  MyInheritedData({
    Key key,
    this.myField,
    this.onMyFieldChange,
    Widget child,
  }) : super(key: key, child: child);

  @override
  bool updateShouldNotify(MyInheritedData oldWidget) {
    return oldWidget.myField != myField ||
        oldWidget.onMyFieldChange != onMyFieldChange;
  }
}

しかし、新しいInheritedWidgetを作成してもツリー全体は再構築されませんか?

いいえ、必ずしもそうとは限りません。新しいInheritedWidgetは潜在的に以前とまったく同じ子を持つことができるため。そして正確には、同じインスタンスを意味します。以前と同じインスタンスを持つウィジェットは再構築されません。

そして、ほとんどの状況(アプリのルートにinheritedWidgetがある場合)では、継承されたウィジェットはconstantです。不要な再構築はありません。

69
Rémi Rousselet

TL; DR

pdateShouldNotifyメソッド内で重い計算を使用せず、ウィジェットの作成時にnewの代わりにconstを使用します


まず、ウィジェット、要素、レンダリングオブジェクトとは何かを理解する必要があります。

  1. Renderオブジェクトは、実際に画面にレンダリングされるものです。それらはmutableであり、ペイントおよびレイアウトロジックが含まれています。レンダーツリーは、Webのドキュメントオブジェクトモデル(DOM)に非常に似ており、このツリーのレンダーオブジェクトをDOMノードとして見ることができます。
  2. Widget-レンダリングする内容の説明です。それらはimmutableで安価です。そのため、ウィジェットが質問「What?」(宣言的アプローチ)に答えると、Renderオブジェクトは質問「How?」(Imperativeアプローチ)に答えます。 Webからの類推は「仮想DOM」です。
  3. Element/BuildContext-WidgetRenderオブジェクト間のプロキシです。ツリー内のウィジェットの位置に関する情報と、対応するウィジェットが変更されたときにRenderオブジェクトを更新する方法に関する情報が含まれています。

これで、InheritedWidgetおよびBuildContextのメソッドinheritFromWidgetOfExactTypeに飛び込む準備ができました。

例として、FlutterのInheritedWidgetに関するドキュメントからこの例を検討することをお勧めします。

class FrogColor extends InheritedWidget {
  const FrogColor({
    Key key,
    @required this.color,
    @required Widget child,
  })  : assert(color != null),
        assert(child != null),
        super(key: key, child: child);

  final Color color;

  static FrogColor of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(FrogColor);
  }

  @override
  bool updateShouldNotify(FrogColor old) {
    return color != old.color;
  }
}

InheritedWidget-この場合、1つの重要なメソッドを実装するウィジェット-updateShouldNotifyupdateShouldNotify-1つのパラメーターoldWidgetを受け入れ、ブール値:trueまたはfalseを返す関数。

他のウィジェットと同様に、InheritedWidgetには対応するElementオブジェクトがあります。 InheritedElementです。新しいウィジェットを作成するたびに、ウィジェットでInheritedElement呼び出しupdateShouldNotify(祖先で(setStateを呼び出し))。 updateShouldNotifyが返す場合true InheritedElementはdependencies(?)を反復処理し、メソッドdidChangeDependenciesその上。

InheritedElementはdependenciesを取得しますか?ここでは、inheritFromWidgetOfExactTypeメソッドを見る必要があります。

inheritFromWidgetOfExactType-BuildContextおよびeveryElementで定義されているこのメソッドは、BuildContextインターフェイス(Element == BuildContext)を実装します。したがって、すべての要素にこのメソッドがあります。

InheritFromWidgetOfExactTypeのコードを見てみましょう。

final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
  assert(ancestor is InheritedElement);
  return inheritFromElement(ancestor, aspect: aspect);
}

ここでは、タイプごとにマッピングされた_ inheritedWidgetsで先祖を見つけようとします。祖先が見つかった場合、inheritFromElementを呼び出します。

inheritFromElementのコード:

  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
  1. 現在の要素の依存関係として祖先を追加します(_dependencies.add(ancestor))
  2. 現在の要素を祖先の依存関係に追加します(ancestor.updateDependencies(this、aspect))
  3. inheritFromWidgetOfExactTypeの結果として祖先のウィジェットを返します(return ancestor.widget)

これで、InheritedElementが依存関係を取得する場所がわかりました。

didChangeDependenciesメソッドを見てみましょう。すべての要素には次のメソッドがあります。

  void didChangeDependencies() {
    assert(_active); // otherwise markNeedsBuild is a no-op
    assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
    markNeedsBuild();
  }

ご覧のとおり、このメソッドは要素をdirtyとしてマークするだけで、この要素は次のフレームで再構築する必要があります。 Rebuildは、対応するウィジェット要素でメソッドを呼び出すbuildを意味します。

しかし、「InheritedWidgetを再構築すると、サブツリー全体が再構築されますか?」ここで、ウィジェットは不変であり、新しいウィジェットを作成すると、Flutterがサブツリーを再構築することを覚えておく必要があります。どうすれば修正できますか?

  1. 手でウィジェットをキャッシュする(手動)
  2. constを使用します。const インスタンスを1つだけ作成 値/クラス
12
maksimr

docs から:

[BuildContext.inheritFromWidgetOfExactType]は、特定のタイプの最も近いウィジェットを取得します。これは、具体的なInheritedWidgetサブクラスのタイプである必要があり、そのウィジェットを変更(またはそのタイプの新しいウィジェットまたはウィジェットがなくなると)、このビルドコンテキストは再構築され、そのウィジェットから新しい値を取得できるようになります。

これは通常、of()静的メソッドから暗黙的に呼び出されます。テーマ。

OPが指摘したように、InheritedWidgetインスタンスは変更されません...しかし、ウィジェットツリーの同じ場所で新しいインスタンスに置き換えることができます。その場合、登録済みのウィジェットを再構築する必要がある可能性があります。 InheritedWidget.updateShouldNotifyメソッドがこの決定を行います。 (参照: docs

では、インスタンスはどのように置き換えられますか? InheritedWidgetインスタンスはStatefulWidgetに含まれる場合があり、古いインスタンスを新しいインスタンスに置き換える場合があります。

2
kkurian