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


102

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

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

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

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

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

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

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

InheritedWidgetにデータを詰め込み、後で変更することができます。その変更が発生すると、私のInheritedWidgetを参照するすべてのウィジェットが再構築されます。

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

回答:


107

問題はあなたの見積もりから来ていますが、これは誤りです。

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

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

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

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

ウィジェット/要素/レンダーボックスがどのように接続されるかについての素晴らしい話があります。しかし、簡単に言うと、これらは次のようになります(左は通常のウィジェット、中央は「要素」、右は「レンダリングボックス」です)。

ここに画像の説明を入力してください

重要なのは、新しいウィジェットをインスタンス化するときです。フラッターは古いものと比較します。RenderBoxを指す「要素」を再利用します。そして、変異し RenderBoxのプロパティを。


わかりましたが、これは私の質問にどのように答えますか?

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

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

あなたがすべてを理解していれば、あなたはすでに解決策を推測しているはずです:

何かが変わったときにいつでも真新しいものを作り出すあなたのInheritedWidget内側を包んでください!StatefulWidgetInheritedWidget

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

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);

  static MyInheritedData of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MyInheritedData>();
  }

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

しかし、新しいInheritedWidgetを作成すると、ツリー全体が再構築されませんか?

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

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


1
しかし、新しいInheritedWidgetを作成すると、ツリー全体が再構築されませんか?では、なぜリスナーが必要なのでしょうか。
トーマス

1
あなたの最初のコメントのために、私は私の答えに3番目の部分を追加しました。退屈なことについては、私は同意しません。コードスニペットはこれをかなり簡単に生成できます。また、データへのアクセスは、呼び出しと同じくらい簡単MyInherited.of(context)です。
レミRousselet

3
興味があるかどうかはわかりませんが、このテクニックを使用してサンプルを更新しました:github.com/brianegan/flutter_architecture_samples/tree/master/…確かに 少し重複が少なくなっています!その実装について他に提案がある場合は、少し余裕があればコードレビューを気に入ってください:)このロジッククロスプラットフォーム(FlutterとWeb)を共有するための最良の方法を見つけて、テスト可能であることを確認します(特に非同期のもの)。
brianegan

4
以来updateShouldNotifyテストは常に同じに言及されたMyInheritedState場合、それは常に戻りませんかfalse?確かにのbuildメソッドMyInheritedStateは新しい_MyInheritedインスタンスを作成していますが、dataフィールドは常にthisnoを参照していますか?問題があります...ハードコードするだけで機能しますtrue
cdock

2
@cdockええ、私の悪い。それが明らかに機能しないので、なぜ私がそれをしたのか覚えていません。trueに編集して修正しました。ありがとうございます。
レミRousselet

20

TL; DR

updateShouldNotifyメソッド内で重い計算を使用せず、ウィジェットの作成時にnewではなく constを使用する


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

  1. レンダーオブジェクトは、画面に実際にレンダリングされるものです。それらは変更可能で、ペインティングとレイアウトロジックが含まれています。レンダーツリーは、Webのドキュメントオブジェクトモデル(DOM)とよく似ており、レンダーオブジェクトをこのツリーのDOMノードとして見ることができます。
  2. ウィジェット -何をレンダリングするかを記述したものです。それらは不変で安価です。したがって、ウィジェットが「何ですか?」(宣言型アプローチ)の質問に答えると、Renderオブジェクトは「どのように?」(命令型アプローチ)の質問に答えます。Webからの類推は「仮想DOM」です。
  3. Element / BuildContext- ウィジェットレンダーオブジェクト間のプロキシです。ツリー内のウィジェットの位置に関する情報と、対応するウィジェットが変更されたときにRenderオブジェクトを更新する方法が含まれています。

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

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

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つの重要なメソッド-updateShouldNotifyを実装する単なるウィジェット。 updateShouldNotify -1つのパラメーターoldWidgetを受け入れ、ブール値(trueまたはfalse)を返す関数。

他のウィジェットと同様に、InheritedWidgetには対応するElementオブジェクトがあります。それはあるInheritedElement。InheritedElementは、新しいウィジェットを作成するたびに、ウィジェットでupdateShouldNotifyを呼び出します(祖先でsetStateを呼び出します)。ときupdateShouldNotifyは返す InheritedElementを反復依存関係(?)と呼び出し方法のdidChangeDependenciesその上を。

InheritedElementはどこで依存関係を取得しますか?ここでは、inheritFromWidgetOfExactTypeメソッドを確認する必要があります。

inheritFromWidgetOfExactType-このメソッドはBuildContextで定義され、 すべての Elementが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の結果として祖先のウィジェットを返します(ancestor.widgetを返します)

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

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

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

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

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

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

1
素晴らしい説明maksimr。最も混乱しているのは、inheritedWidgetが置き換えられたときにサブツリー全体がとにかく再構築された場合、updateShouldNotify()のポイントは何ですか?
Panda World

3

ドキュメントから:

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

これは通常、theme.ofなどのof()静的メソッドから暗黙的に呼び出されます。

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

では、インスタンスはどのように置き換えられるのでしょうか?InheritedWidgetインスタンスがで含有させることができるStatefulWidget新しいインスタンスに古いインスタンスを置き換える可能性があります。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.