Flutter ノート | Flutter 機能コンポーネント

インターセプトリターンキー (WillPopScope)

ユーザーが誤って「戻る」ボタンに触れてアプリが終了してしまうことを防ぐため、多くのアプリではユーザーが「戻る」ボタンをクリックするボタンをインターセプトし、例えばユーザーが2回クリックした場合などに何らかのミスタッチ判定を行っています。一定時間内に、ユーザーは (誤タップではなく) ログアウトするつもりだったと想定します。

Flutter では、WillPopScope戻るボタンのインターセプトを実装できます。WillPopScopeデフォルトのコンストラクターを見てみましょう。

const WillPopScope({
    
    
  ...
  required WillPopCallback onWillPop,
  required Widget child
})

onWillPopユーザーが戻るボタン (ナビゲーションの戻るボタンや Android の物理的な戻るボタンを含む) をクリックしたときに呼び出されるコールバック関数です。コールバックはFutureオブジェクトを返す必要があります。返されたFuture最終値falseが の場合、現在のルートはスタックから出ません (戻りません)。true最終値が の場合、現在のルートはスタックから出ます。終了するかどうかを決定するには、このコールバックを提供する必要があります。

: ユーザーが誤ってリターン キーを押して終了するのを防ぐために、リターン イベントをインターセプトします。ユーザーが 1 秒以内に [戻る] ボタンを 2 回クリックすると終了しますが、間隔が 1 秒を超えると終了せず、計測が再開されます。

コードは以下のように表示されます。

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

  
  WillPopScopeTestRouteState createState() {
    
    
    return WillPopScopeTestRouteState();
  }
}

class WillPopScopeTestRouteState extends State<WillPopScopeTestRoute> {
    
    
  DateTime? _lastPressedAt; //上次点击时间

  
  Widget build(BuildContext context) {
    
    
    return WillPopScope(
        onWillPop: () async {
    
    
          if (_lastPressedAt == null ||
              DateTime.now().difference(_lastPressedAt!) > const Duration(seconds: 1)) {
    
    
            // 两次点击间隔超过1秒则重新计时
            _lastPressedAt = DateTime.now();
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: const Text("再按一次退出页面"),
                action: SnackBarAction(label: "确定", onPressed: () => {
    
    },),
                duration: const Duration(milliseconds: 1000),
              ),
            );
            return false;
          }
          return true;
        },
        child: Container(
          alignment: Alignment.center,
          child: const Text("1秒内连续按两次返回键退出"),
        )
    );
  }
}

データ共有 (InheritedWidget)

InheritedWidgetwidgetこれは Flutter の非常に重要な機能コンポーネントです。ツリーの上から下までデータを共有する方法を提供します。たとえば、アプリケーションのルートでデータの一部を共有する場合、それどのでもwidget使用できますそのシェアのデータを取得してください! この機能は、ツリー全体でデータを共有する必要があるシナリオで非常に便利です。たとえば、アプリケーションのテーマ(現在のロケール) 情報はFlutter SDK を通じて共有されます。InheritedWidgetwidgetwidgetInheritedWidgetThemeLocale

InheritedWidgetReactの関数と同様にcontext、コンポーネントはレベルごとにデータを渡すのではなく、レベル間でデータを受け渡すことができます。ツリーInheritedWidget内のデータ転送の方向は上から下であり、通知転送の方向とはまったく逆です。widgetNotification

私たちのバージョンの「Counter」サンプル アプリケーションを見てみましょうInheritedWidgetInheritedWidgetこの例は主にカウンタの機能特性を示すためのものであり、カウンタの推奨実装ではないことに注意してください。

まず、InheritedWidget現在のカウンター クリック数を継承を通じてプロパティShareDataWidgetのプロパティに保存しますdata

class ShareDataWidget extends InheritedWidget {
    
    
  ShareDataWidget({
    
    Key? key, required this.data, required Widget child,}) : super(key: key, child: child);

  final int data; // 需要在子树中共享的数据,保存点击次数

  // 定义一个便捷方法,方便子树中的widget获取共享数据
  static ShareDataWidget? of(BuildContext context) {
    
    
    return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
  }

  // 该回调决定当data发生变化时,是否通知子树中依赖data的Widget重新build
  
  bool updateShouldNotify(ShareDataWidget old) {
    
    
    return old.data != data;
  }
}

次に、メソッド_TestWidget内でデータを参照するサブコンポーネントを実装します同時に、コールバックでログを出力します。buildShareDataWidgetdidChangeDependencies()

class _TestWidget extends StatefulWidget {
    
    
  
  _TestWidgetState createState() => _TestWidgetState();
}

class _TestWidgetState extends State<_TestWidget> {
    
    

  
  Widget build(BuildContext context) {
    
    
    // 使用InheritedWidget中的共享数据
    return Text(ShareDataWidget.of(context)!.data.toString());
  }

    
  void didChangeDependencies() {
    
    
    super.didChangeDependencies();
    // 父或祖先widget中的InheritedWidget改变(updateShouldNotify返回true)时会被调用。
    // 如果build中没有依赖InheritedWidget,则此回调不会被调用。
    print("Dependencies change");
  }
}

DidChangeDependency コールバック:

  • 前に紹介したStatefulWidgetライフサイクルで、StateオブジェクトにはdidChangeDependenciesコールバックがあると述べました。このコールバックは、「依存関係」が変更されたときに Flutter フレームワークによって呼び出されます。そして、この「依存関係」とは、widget子が親のデータを使用するかどうかをwidget指しますInheritedWidgetこれが使用されている場合は、子にwidget依存関係があることを意味し、使用されていない場合は、依存関係がないことを意味します。

  • このメカニズムにより、InheritedWidget依存関係が変更されたときにサブコンポーネント自体を更新できるようになります。たとえば、テーマやロケール(言語)などが変更されると、依存する子widgetのメソッドdidChangeDependenciesが呼び出されます。

最後に、クリックされるたびにShareDataWidget値が増加するボタンを作成します。

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

  
  State createState() => _InheritedWidgetTestRouteState();
}

class _InheritedWidgetTestRouteState extends State<InheritedWidgetTestRoute> {
    
    
  int count = 0;

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      appBar: AppBar(
        title: const Text("ShareDataWidget"),
      ),
      body: Center(
        child: ShareDataWidget(
          // 使用ShareDataWidget
          data: count,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Padding(
                padding: EdgeInsets.only(bottom: 20.0),
                child: _TestWidget(), // _TestWidget中依赖ShareDataWidget
              ),
              ElevatedButton(
                child: const Text("Increment"),
                // 每点击一次,将count自增,然后重新build, ShareDataWidget的data将被更新
                onPressed: () => setState(() => ++count),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

ここに画像の説明を挿入

ボタンをクリックするたびに、カウンターが自動的に増加し、コンソールにログが出力されます。

I/flutter ( 8513): Dependencies change

これは、表示される依存関係が変更された後にdidChangeDependencies()呼び出されます。ただし、メソッド内でデータが使用されていない場合は_TestWidget依存関係がないため呼び出されないことbuildShareDataWidgetdidChangeDependencies()ShareDataWidget注意してください

たとえば、_TestWidgetStateコードを次のように変更すると、didChangeDependencies()呼び出されなくなります。

class _TestWidgetState extends State<_TestWidget> {
    
    
  
  Widget build(BuildContext context) {
    
     
     return Text("text");
  }

  
  void didChangeDependencies() {
    
    
    super.didChangeDependencies();
    // build方法中没有依赖InheritedWidget,此回调不会被调用。
    print("Dependencies change");
  }
}

上記のコードでは、build()メソッド内の依存コードを削除しShareDataWidget、固定値を返すTextようにしています。これによりIncrement、ボタンがクリックされたときに、ShareDataWidgetメソッドはdata変更されますが、_TestWidgetState依存していないためShareDataWidget_TestWidgetStateメソッドdidChangeDependenciesは呼び出されません。実際、データが変更されたときにそのデータを使用するウィジェットのみを更新するのが合理的でパフォーマンスに優しいため、このメカニズムは理解しやすいです。

DidChangeDependency() では何をすべきでしょうか?

一般に、 Flutter フレームワークはwidget依存関係の変更後にbuild()コンポーネント ツリーを再構築するメソッドも呼び出すため、子がこのメソッドをオーバーライドすることはほとんどありません。build()ただし、依存関係の変更後にネットワーク要求などの高コストの操作を実行する必要がある場合は、これらの高コストの操作を毎回実行することを避けるために、このメソッドでそれらの操作を実行するのが最善の方法です。

InheritedWidget について詳しく見る

上の例で、 のデータを_TestWidgetState参照するだけで、変更があったときにメソッドを呼び出しShareDataWidgetたくない場合はどうなるでしょうか。実際、答えは非常に簡単で、次の実装を変更するだけで済みます。ShareDataWidget_TestWidgetStatedidChangeDependencies()ShareDataWidget.of()

// 定义一个便捷方法,方便子树中的widget获取共享数据
static ShareDataWidget of(BuildContext context) {
    
    
  //return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
  return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget;
}

唯一の変更は、ShareDataWidgetオブジェクトを取得するメソッドと、dependOnInheritedWidgetOfExactType()メソッドが置き換えられたことcontext.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widgetです。では、これらの違いは何ですか? これら 2 つのメソッドのソース コードを見てみましょう (実装コードはElementクラス内にあります)。


InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
    
    
  final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
  return ancestor;
}

InheritedWidget dependOnInheritedWidgetOfExactType({
    
     Object aspect }) {
    
    
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
  //多出的部分
  if (ancestor != null) {
    
    
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
  }
  _hadUnsatisfiedDependencies = true;
  return null;
}

以下のように、ソースコードdependOnInheritedWidgetOfExactType()よりもメソッドgetElementForInheritedWidgetOfExactType()が調整されていることがわかります。dependOnInheritedElementdependOnInheritedElement

  
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, {
    
     Object aspect }) {
    
    
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }

主にメソッドに依存関係が登録されていることがわかりますdependOnInheritedElementこれを見ると、と の呼び出しの違いは、前者は依存関係を登録するのに対し、後者は依存関係を登録しないことですdependOnInheritedWidgetOfExactType()getElementForInheritedWidgetOfExactType()

したがって、 を呼び出すとdependOnInheritedWidgetOfExactType()InheritedWidgetそれに依存する子孫コンポーネントとの関係が登録され、InheritedWidget変更があった場合には、依存する子孫コンポーネントが更新される、つまり、それらの子孫コンポーネントのdidChangeDependencies()メソッドやbuild()メソッドが調整されます。 。

そして、それが呼び出されたときgetElementForInheritedWidgetOfExactType()、登録された依存関係がないため、InheritedWidget後で変更があった場合、対応する子孫は更新されませんWidget

ShareDataWidget.of()上記の例のメソッド実装を call に変更しgetElementForInheritedWidgetOfExactType()、「 」ボタンをクリックすると、メソッドは再度呼び出されませんIncrementが、引き続き呼び出されることに注意してください。この理由は、実際には、「 」ボタンをクリックした後、メソッドが呼び出されこのときにページ全体が再構築されるためです。この例ではキャッシュがないため、キャッシュも再構築されます。とも呼ばれます。_TestWidgetStatedidChangeDependencies()build()Increment_InheritedWidgetTestRouteStatesetState()_TestWidgetbuild()

そこで、ここで問題が発生します。実際、ShareDataWidgetサブツリー内の依存コンポーネントを更新したいだけであり、メソッドが呼び出されている限り_InheritedWidgetTestRouteStatesetState()すべての子ノードが再作成されますbuildが、これは不要なので、何か方法はありますか?避けてください?毛織物? 答えはキャッシングです!簡単な方法は、サブツリーをカプセル化してキャッシュすることです(具体的な方法は、 StatefulWidgetWidgetWidgetを通じてそれを実現する方法で後で紹介しますProvider)。

InheritedWidget のソース コード分析

一般に、dependOnInheritedWidgetOfExactTypeこのメソッドは子ノードが祖先ノードからデータを取得するためのエントリ ポイントであるため、分析のエントリ ポイントでもあり、そのロジックをコード リスト 8 ~ 9 に示します。

// 代码清单8-9 flutter/packages/flutter/lib/src/widgets/framework.dart
 // Element
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({
    
    Object? aspect}) {
    
    
    // 从_inheritedWidgets中获取指定Widget类型的InheritedElement,生成逻辑见代码清单8-14
	final InheritedElement? ancestor =  _inheritedWidgets == null ? null : _inheritedWidgets![T];
    if (ancestor != null) {
    
    
       return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
}

InheritedWidget dependOnInheritedElement(InheritedElement ancestor, {
    
     Object? aspect }) {
    
    
  	_dependencies ??= HashSet<InheritedElement>(); // 记录自身所依赖的InheritedElement节点
  	_dependencies!.add(ancestor); // 新增一个依赖
  	ancestor.updateDependencies(this, aspect); // 告知被依赖节点当前节点请求依赖,见代码清单8-10 
  	return ancestor.widget; // 返回T类型的Widget节点
}

上記のロジックは、まず、そこから最も近いタイプのノードを取得します。なぜ最新なのかについては、後ほど分析します。取得されたノードは、現在のノードが依存するノードです。メソッドの主なロジックは、依存するすべてのノードを含む現在のノードのフィールドを取り出すことです。この時点で、オブジェクトが追加されコード リストに示すようにメソッドが呼び出されます。 8-10。_inheritedWidgetsElement TreeTInheritedElementancestorElementdependOnInheritedElement_dependenciesInheritedElementancestorancestorupdateDependencies

// 代码清单8-10 flutter/packages/flutter/lib/src/widgets/framework.dart
class InheritedElement extends ProxyElement {
    
    

  final Map<Element, Object?> _dependents = HashMap<Element, Object?>();
  
   // dependent 即代码清单8-9中调用本方法的对象
  void updateDependencies(Element dependent, Object? aspect) {
    
    
    setDependencies(dependent, null); 
  }
  
  
  void setDependencies(Element dependent, Object? value) {
    
    
    // 通过_dependents记录了所有依赖自身的dependent节点
    // 以便自身数据更新时能通知到该节点,详见代码清单8-12
    _dependents[dependent] = value;  
  } 
}

上記のロジックは主に現在のノードをフィールドに追加することであるためancestor_dependents図 8-2 に示すように、依存ノードと依存ノードの両方が相互に記録します。
ここに画像の説明を挿入

では、このデータ構造に基づいて、ancestor自身のデータが変更されたときに対応するコールバックをトリガーするにはどうすればよいでしょうか? InheritedElement分析する最初の方法updateは、コード リスト 8 ~ 11 に示すように、データ変更により自身のエントリの更新を開始することです。

// 代码清单8-11 flutter/packages/flutter/lib/src/widgets/framework.dart
abstract class ProxyElement extends ComponentElement {
    
    
  
  void update(ProxyWidget newWidget) {
    
     // 在Build流程中触发
    final ProxyWidget oldWidget = widget as ProxyWidget; // 记录旧的Widget配置
    super.update(newWidget);
    updated(oldWidget); 
    rebuild(force: true);  
  }
  void rebuild({
    
    bool force = false}) {
    
    
    ...
    try {
    
    
      performRebuild();
    } finally {
    
    
      ...
    }
    ...
  }
   
  void performRebuild() {
    
    
    _dirty = false; // 标记为需要重新进行Build流程
  }
  
  void updated(covariant ProxyWidget oldWidget) {
    
    
    notifyClients(oldWidget); // 见代码清单8-12
  }
  
  Widget build() => widget.child; // 即被代理的Widget,该Widget在InheritedWidget初始化时传入
} 

class InheritedElement extends ProxyElement {
    
    
   // updated方法是ProxyElement特有的,注意与update方法区分
  
  void updated(InheritedWidget oldWidget) {
    
    
    // updateShouldNotify是为InheritedWidget的子类提供一个控制依赖更新条件的入口 
    if ((widget as InheritedWidget).updateShouldNotify(oldWidget)) {
    
    
      super.updated(oldWidget);
    }
  }
} 

上記のロジックでは、メソッドが最初に呼び出されupdatedメソッドがnotifyClientsトリガーされます。このメソッドは最終的に独自のメソッドを呼び出しますが、その役割自体がエージェントであり、特定のビルドプロセス ロジックがエージェント内にあるため、メソッドはその子を直接返すことがわかりますさらに、のコンストラクターはによって変更され、対応するサブツリーは次のビルドプロセスで直接保持されますdidChangeDependenciesrebuildbuildProxyElementbuildWidgetWidgetInheritedWidgetconstElement Tree

では、実際に影響を受ける子ノードはどのように更新されるのでしょうか? リスト 8-12 に示すように、まずnotifyClientsメソッドを分析します。

// 代码清单8-12 flutter/packages/flutter/lib/src/widgets/framework.dart
class InheritedElement extends ProxyElement {
    
    
  
  void notifyClients(InheritedWidget oldWidget) {
    
    
  	// 这里的_dependents.keys记录了依赖它的所有Element节点
    for (final Element dependent in _dependents.keys) {
    
     // 注册逻辑见代码清单8-10
      notifyDependent(oldWidget, dependent);
    }
  }
  
  void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    
    
    dependent.didChangeDependencies(); // 触发依赖节点的回调,见代码清单8-13
  }
}

コード リスト 8 ~ 13 に示すように、上記のロジックは主に、_dependentsすべてのフィールドkey、つまり現在のノードに依存するすべてのオブジェクトを走査しElement、それらのメソッドを呼び出すことです。didChangeDependencies

// 代码清单8-13 flutter/packages/flutter/lib/src/widgets/framework.dart
class StatefulElement extends ComponentElement {
    
    
  
  void didChangeDependencies() {
    
    
    super.didChangeDependencies(); // 第1步,Element的逻辑,触发Build流程
    _didChangeDependencies = true; // 标记当前节点依赖改变,对应代码清单8-3中的判断
  }
}
abstract class Element extends DiagnosticableTree implements BuildContext {
    
    
  
  void didChangeDependencies() {
    
    
    markNeedsBuild(); // 标记当前节点需要更新
  }
  void markNeedsBuild() {
    
    
    if (_lifecycleState != _ElementLifecycle.active) return; // 状态异常
    if (dirty) return; // 已经标记
    _dirty = true; // 标记为需要重新进行Build流程
    owner!.scheduleBuildFor(this); // 见代码清单5-45
  }
}

上記のロジックでは、1最初のステップで渡されたメソッドは、依存ノードを としてElementマークし、フレームの更新を要求します。次に、コード リスト 8-3 からわかるように、現在のノードのフィールドを としてマークします。 については、そのおよびメソッドのコールバックが順番にトリガーされます。markNeedsBuilddirtyElement_didChangeDependenciestrueStatefulElementdidChangeDependenciesbuild

// 代码清单8-3 flutter/packages/flutter/lib/src/widgets/framework.dart
 // StatefulElement
void performRebuild() {
    
    
  if (_didChangeDependencies) {
    
     // 通常在代码清单8-13中设置为true,详见8.2节
    state.didChangeDependencies(); // 当该字段为true时触发didChangeDependencies回调
    _didChangeDependencies = false;
  }
  super.performRebuild(); // 父类该方法中会调用build()方法
} 

Widget build() => state.build(this); // 由上面super.performRebuild()触发

以上がInheritedWidget工夫であり、部分リフレッシュElement Treeは2 つのフィールドによって実現されています 図 8-2 を例にとると、データが変更された場合、そのサブツリーは完全に再構築されるのではなく、サブツリーのみが再構築されます。Element AElement B

最後に、_inheritedWidgetsリスト 8-9 のコードがどのように生成されたかを分析します。Element Treeノードが新しくマウントされると、_updateInheritanceリスト 8-14 に示すようにメソッドがトリガーされます。

// 代码清单8-14 flutter/packages/flutter/lib/src/widgets/framework.dart
class InheritedElement extends ProxyElement {
    
    
  
  void _updateInheritance() {
    
     // 见代码清单5-3
    final Map<Type, InheritedElement>? incomingWidgets = _parent?._inheritedWidgets;
    if (incomingWidgets != null) // 继承父节点的可用依赖,即InheritedWidget的子类集合
      _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
    else // 新建一个空的集合
      _inheritedWidgets = HashMap<Type, InheritedElement>();
    _inheritedWidgets![widget.runtimeType] = this; // 记录当前节点,注意该操作会覆盖类型相同的节点 
  }
}
abstract class Element extends DiagnosticableTree implements BuildContext {
    
    
  void _updateInheritance() {
    
      // InheritedElement重写该方法并添加自身作为一个可用依赖
    _inheritedWidgets = _parent?._inheritedWidgets; // 默认逻辑,继承父类的可用依赖
  }
}

上記のロジックは実際には非常に明確です。各ノードは、InheritedElement対応するWidget基づいてコレクションKeyに自身を追加します_inheritedWidgetsが、他の型の場合は、Element親ノードの情報を直接継承します_inheritedWidgetsしたがって、子ノードB Widgetの場合にのみ、によってローカル リフレッシュを完了できます。A WidgetInheritedWidget

破棄ロジックに関しては、ノードが削除されるとメソッドがトリガーされ、現在のノードが依存するすべての親ノードのデータ構造ElementからElement Tree削除されますElementdeactivate()ElementMap

// 代码清单8-7 flutter/packages/flutter/lib/src/widgets/framework.dart

void deactivate() {
    
     // Element
  if (_dependencies != null && _dependencies!.isNotEmpty) {
    
     // 依赖清理
    for (final InheritedElement dependency in _dependencies!)
      dependency._dependents.remove(this);
  }
  _inheritedWidgets = null;
  _lifecycleState = _ElementLifecycle.inactive; // 更新状态
}

InheritedWidget以上が上記の謎のすべてです。

要約:

この方法によりdependOnInheritedWidgetOfExactType、子ノードと親ノードは相互に記録し、データが変更されると、親ノードはオブザーバーモードを通じて、依存するすべての子ノードに更新を通知します。

  • 依存する親ノードの場合、すべての依存する子ノードが_dependentsこのMapフィールドを通じて記録されます。key

  • 依存する子ノードの場合InheritedWidget_inheritedWidgetsこのMapフィールドには、現在のタイプに対応するオブジェクトがその親ノードのkey-value形式で保存されるか、親ノードから直接継承されます (親ノードに利用可能な依存関係情報がある場合)。WidgetElement

  • 更新が必要な場合、メソッドはビルド プロセスでトリガーされます。このメソッドの最終的な呼び出しロジックは、それぞれInheritedElementthisを走査します。つまり、それに依存するすべての子ノード オブジェクトを取得し、それぞれのメソッドを呼び出します。子ノード。update_dependentsMapkeyElementElementdidChangeDependencies

    これにより、メソッドがノードを としてマークし、フレームの更新を要求しますStatefulElementmarkNeedsBuildこれdirtyにより、最終的にメソッドと、StatefulWidget対応するStateクラス オブジェクト ( StatefulElementholding)のdidChangeDependenciesメソッドbuild実行がトリガーされます。

コンポーネント間で状態を共有する

状態とイベントを同期する

Flutter 開発において、状態管理は永遠のテーマです。一般原則は、状態がコンポーネントにとってプライベートである場合、状態はコンポーネント自体によって管理される必要があり、状態がコンポーネント間で共有される場合、状態は各コンポーネントの共通の親要素によって管理される必要があります。コンポーネントのプライベートな状態管理を理解するのは簡単ですが、コンポーネント間で共有される状態については、オブザーバー モードの実装であるグローバル イベント バスevent_busを使用するなど、より多くの管理方法があります。同期: 状態保持者 (パブリッシャー) は状態の更新と公開を担当し、状態ユーザー (オブザーバー) は状態変更イベントをリッスンして一部の操作を実行します。ログインステータス同期の簡単な例を見てみましょう。

イベントを定義します。

enum Event{
    
    
  login,
  ... //省略其他事件
}

ログイン ページのコードはおおよそ次のとおりです。

// 登录状态改变后发布状态改变事件
bus.emit(Event.login);

ログインステータスに依存するページ:

void onLoginChanged(e){
    
    
  //登录状态变化处理逻辑
}


void initState() {
    
    
  //订阅登录状态改变事件
  bus.on(Event.login,onLogin);
  super.initState();
}


void dispose() {
    
    
  //取消订阅
  bus.off(Event.login,onLogin);
  super.dispose();
}

オブザーバー パターンを通じてコン​​ポーネント間の状態共有を実現するには、明らかな欠点がいくつかあることがわかります。

  1. さまざまなイベントを明示的に定義する必要があり、管理は容易ではありません。
  2. サブスクライバは状態変更コールバックを明示的に登録する必要があり、メモリ リークを避けるためにコンポーネントが破棄されたときにコールバックのバインドを手動で解除する必要があります。

Flutter でコンポーネント全体の状態を管理するより良い方法はありますか? 答えは「はい」です。では、どうすればよいでしょうか? 先ほど紹介したことを考えてみましょう。その本来の特徴は、子孫コンポーネントとの依存関係をInheritedWidgetバインドできること、そしてデータが変更された場合、依存する子孫コンポーネントを自動的に更新できることです。この機能を使用すると、 のコンポーネント間で共有する必要がある状態を保存し、サブコンポーネントでそれを参照できます。Flutter コミュニティでよく知られているプロバイダーパッケージは、このアイデアに基づいたコンポーネント間の状態共有ソリューションのセットです。 . 次に使い方と原理を詳しく紹介します。InheritedWidgetInheritedWidgetInheritedWidgetInheritedWidgetProvider

プロバイダー

Providerは Flutter の公式の状態管理パッケージです。読者の原理の理解を強化するために、Provider パッケージのソース コードを直接見るのではなく、次のような考え方で最小限の機能の Provider を段階的に実装しますInheritedWidget。実装。

ミニプロバイダーのカスタム実装

まず第一に、共有データを保存できるものが必要ですInheritedWidget。特定のビジネス データ型は予測できないため、汎用性を高めるために、ジェネリックを使用して、InheritedProvider以下を継承する一般クラスを定義しますInheritedWidget

// 一个通用的InheritedWidget,保存需要跨组件共享的状态
class InheritedProvider<T> extends InheritedWidget {
    
    
  InheritedProvider({
    
    required this.data, required Widget child}) : super(child: child);

  final T data;

  
  bool updateShouldNotify(InheritedProvider<T> old) {
    
    
    //在此简单返回true,则每次更新都会调用依赖其的子孙节点的`didChangeDependencies`。
    return true;
  }
}

データを保存する場所ができたので、次に行う必要があるのは、データが変更されたときに再構築することですが、InheritedProviderここで 2 つの問題に直面します。

  1. データの変更を通知するにはどうすればよいですか?
  2. 誰が再建するのかInheritedProvider

最初の問題は実際には簡単に解決できます。もちろん、イベント通知に以前の導入を使用することもできますが、Flutter 開発eventBus近づけるために、Flutter SDK で提供されるクラスを使用します。スタイル パブリッシャー - サブスクライバー モードの場合、定義はおおよそ次のとおりです。ChangeNotifierListenableChangeNotifier

class ChangeNotifier implements Listenable {
    
    
  List listeners = [];
  
  
  void addListener(VoidCallback listener) {
    
     
     listeners.add(listener);  // 添加监听器
  }
  
  
  void removeListener(VoidCallback listener) {
    
     
    listeners.remove(listener); // 移除监听器
  }
  
  void notifyListeners() {
    
     
    listeners.forEach((item)=>item()); // 通知所有监听器,触发监听器回调 
  } 
  ... //省略无关代码
}

addListener()および を呼び出すことでremoveListener()リスナー (サブスクライバ) を追加および削除できます。notifyListeners()すべてのリスナー コールバックは を呼び出すことで通知できます。

ここで、共有する状態をクラスに入れてModel、それを から継承させます。これにより、共有状態が変更されたときに、サブスクライバに通知するためにChangeNotifier呼び出すだけで済みます。これが 2 番目の問題です。答え!次に、サブスクライバー クラスを実装します。notifyListeners()InheritedProvider

class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget {
    
    
  const ChangeNotifierProvider({
    
    Key? key, required this.data, required this.child,}) : super(key: key);

  final Widget child;
  final T data;

  // 定义一个便捷方法,方便子树中的widget获取共享数据
  static T of<T>(BuildContext context) {
    
    
    final provider = context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>();
    return provider.data;
  }

  
  State createState() => _ChangeNotifierProviderState<T>();
}

このクラスは を継承しStatefulWidgetツリーに保存された共有状態 (モデル) をof()簡単に取得するためにサブクラスの静的メソッドを定義します。以下では、このクラスの対応するクラスを実装します。WidgetInheritedProvider_ChangeNotifierProviderState

class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider<T>> {
    
    

  void update() {
    
    
    //如果数据发生变化(model类调用了notifyListeners),重新构建InheritedProvider
    setState(() => {
    
    });
  }

  
  void didUpdateWidget(ChangeNotifierProvider<T> oldWidget) {
    
    
    //当Provider更新时,如果新旧数据不"==",则解绑旧数据监听,同时添加新数据监听
    if (widget.data != oldWidget.data) {
    
    
      oldWidget.data.removeListener(update);
      widget.data.addListener(update);
    }
    super.didUpdateWidget(oldWidget);
  }

  
  void initState() {
    
     
    widget.data.addListener(update); // 给model添加监听器
    super.initState();
  }

  
  void dispose() {
    
     
    widget.data.removeListener(update); // 移除model的监听器
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    
    
    return InheritedProvider<T>(
      data: widget.data,
      child: widget.child,
    );
  }
}

_ChangeNotifierProviderStateこのクラスの主な機能は、Widget共有状態 (モデル) の変更をリッスンするときにツリーを再構築することであることがわかります。クラス内のメソッドの_ChangeNotifierProviderState呼び出しは常に同じであるため、実行されるときの参照は常に同じ sub であるため、繰り返されないことに注意してください。これはキャッシュと同等です。もちろん、親が再起動されると、受信値が変更される可能性があります。setState()widget.childbuildInheritedProviderchildwidgetwidget.childbuildchildChangeNotifierProviderWidgetbuildchild

必要なツール クラスがすべて完成したので、ショッピング カートの例を使用して、上記のクラスの使用方法を見てみましょう。

ショッピング カート内のすべての商品の合計価格を表示する関数を実装する必要があります。新しい商品がショッピング カートに追加されると、合計価格が更新されます。

Item商品情報を表すクラスを定義します。

class Item {
    
    
  Item(this.price, this.count);
  double price; //商品单价
  int count; // 商品份数
  //... 省略其他属性
}

ショッピング カート内の商品データを保存するクラスを定義しますCartModel

class CartModel extends ChangeNotifier {
    
    
  // 用于保存购物车中商品列表
  final List<Item> _items = [];

  // 禁止改变购物车里的商品信息
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  // 购物车中商品的总价
  double get totalPrice =>
      _items.fold(0, (value, item) => value + item.count * item.price);

  // 将 [item] 添加到购物车。这是唯一一种能从外部改变购物车的方法。
  void add(Item item) {
    
    
    _items.add(item);
    // 通知监听器(订阅者),重新构建InheritedProvider, 更新状态。
    notifyListeners();
  }
}

CartModelつまり、クラスはコンポーネント間で共有されますmodel最後にサンプルページを構築します。

class ProviderRoute extends StatefulWidget {
    
    
  
  _ProviderRouteState createState() => _ProviderRouteState();
}

class _ProviderRouteState extends State<ProviderRoute> {
    
    
  
  Widget build(BuildContext context) {
    
    
    return Center(
      child: ChangeNotifierProvider<CartModel>(
        data: CartModel(),
        child: Builder(builder: (context) {
    
    
          return Column(
            children: <Widget>[
              Builder(builder: (context){
    
    
                var cart = ChangeNotifierProvider.of<CartModel>(context);
                return Text("总价: ${
      
      cart.totalPrice}");
              }),
              Builder(builder: (context){
    
    
                print("ElevatedButton build"); //在后面优化部分会用到
                return ElevatedButton(
                  child: Text("添加商品"),
                  onPressed: () {
    
    
                    // 给购物车中添加商品,添加后总价会更新
                    ChangeNotifierProvider.of<CartModel>(context).add(Item(20.0, 1));
                  },
                );
              }),
            ],
          );
        }),
      ),
    );
  }
}

実行結果:

ここに画像の説明を挿入

「商品追加」ボタンをクリックするごとに合計金額が20ずつ増えていき、期待通りの機能が実現されました!しかし、このような単純な機能を実現するために多くの時間を費やすことに意味があるのでしょうか? 実際、この例に関する限り、同じルーティング ページ内の状態を更新するだけでは、ChangeNotifierProvider使用する利点は明らかではありませんが、ショッピング アプリを作成する場合はどうなるでしょうか。たとえば、ショッピング カートのデータは通常、APP 全体で共有されるため、ルートをまたいで共有されます。ChangeNotifierProviderこれをアプリケーション ツリー全体のルートに配置するWidget、APP 全体でショッピング カートのデータを共有できるようになり、ChangeNotifierProviderその利点は非常に明白になります。

上記の例は比較的単純ですが、Providerプロセスの原理とプロセスを明確に反映しています。上記のコードには多くのクラスが含まれるため、次の図で理解できます。

ここに画像の説明を挿入

簡略化すると以下のようになります。

ここに画像の説明を挿入

Model変更は自動的に通知され(ChangeNotifierProviderサブスクライバー)、ChangeNotifierProvider内部が再構築されInheritedWidget、依存する子孫が更新されます。InheritedWidgetWidget

Providerこれを使用すると、次のような利点があることがわかります。

  1. 私たちのビジネス コードはデータにさらに注意を払っており、データが更新されている限り、Model状態が変化した後に手動で呼び出してsetState()ページを明示的に更新するのではなく、UI は自動的に更新されます。
  2. データ変更のメッセージ配信はブロックされ、状態変更イベントのパブリケーションとサブスクリプションを手動で処理する必要はありません。これらはすべて にカプセル化されていますProviderこれは本当に素晴らしいことであり、多くの作業を節約できます。
  3. 大規模で複雑なアプリケーション、特にグローバルに共有する必要がある状態が多数ある場合、これを使用するとProviderコード ロジックが大幅に簡素化され、エラーの可能性が減り、開発効率が向上します。

問題の最適化

上記の実装には、ChangeNotifierProviderコード構成の問題とパフォーマンスの問題という 2 つの明らかな欠点があります。

1. コード構成の問題

最初に合計価格テキストを作成して表示するコードを見てみましょう。

Builder(builder: (context){
    
    
  var cart=ChangeNotifierProvider.of<CartModel>(context);
  return Text("总价: ${
      
      cart.totalPrice}");
})

このコードは次の 2 つの方法で最適化できます。

  1. ChangeNotifierProvider.ofAPP に多くの内部依存関係がある場合CartModelそのようなコードは非常に冗長になります。
  2. セマンティクスは明確ではありません。ChangeNotifierProviderサブスクライバであるため、依存するものは当然サブスクライバCartModelでありWidget、実際にはステートのコンシューマです。それを使用してBuilder構築する場合、セマンティクスはあまり明確ではありません。明確なセマンティクスを使用できるかどうか最終的なコードのセマンティクスが非常に明確になるWidgetようにConsumer、これを見る限りConsumer、それがコンポーネント間の状態またはグローバルな状態に依存していることがわかります。

これら 2 つの問題を最適化するために、Consumer次のようにウィジェットをカプセル化できます。

// 这是一个便捷类,会获得当前context和指定数据类型的Provider
class Consumer<T> extends StatelessWidget {
    
    
  const Consumer({
    
    Key? key, required this.builder}) : super(key: key);

  final Widget Function(BuildContext context, T value) builder;

  
  Widget build(BuildContext context) {
    
    
    return builder(context, ChangeNotifierProvider.of<T>(context)); // 自动获取Model
  }
}

ConsumerChangeNotifierProvider.of実装は非常に単純で、テンプレート パラメーターを指定して対応するものを取得し、自動的に内部で呼び出しますModelConsumer名前自体は正確なセマンティクス (コンシューマー) を持ちます。上記のコード ブロックは次のように最適化できます。

Consumer<CartModel>(
  builder: (context, cart)=> Text("总价: ${
      
      cart.totalPrice}");
)

2. パフォーマンスの問題

上記のコードには、「追加ボタン」を構築するコードだけでパフォーマンスの問題もあります。

Builder(builder: (context) {
    
    
  print("ElevatedButton build"); // 构建时输出日志
  return ElevatedButton(
    child: Text("添加商品"),
    onPressed: () {
    
    
      ChangeNotifierProvider.of<CartModel>(context).add(Item(20.0, 1));
    },
  );
}

[商品を追加] ボタンをクリックすると、ショッピング カートの合計価格が変更されるため、Text表示される合計価格の更新は予想どおりですが、[商品を追加] ボタン自体は変更されていないため、リセットしないでください。buildしかし、この例を実行すると、[製品の追加] ボタンをクリックするたびにコンソールにログが出力されますElevatedButton build。これは、[製品の追加] ボタンがクリックされるたびに自動的にリセットされることを意味しますbuild

どうしてこれなの?すでに更新メカニズムを理解している場合はInheritedWidget、答えが一目でわかります。これは、 build で呼び出されるからです。ElevatedButtonつまりBuilderChangeNotifierProvider.ofツリーWidget上のInheritedWidget(ie ) に依存するInheritedProviderため、製品が変更があった場合CartModel通知されChangeNotifierProviderChangeNotifierProviderサブツリーが再構築されるため、InheritedProvider更新され、その依存子孫Widgetが再構築されます。

問題の原因は明らかになったので、不要なリファクタリングを回避するにはどうすればよいでしょうか。buildボタンとInheritedWidget依存関係が確立されるとボタンがリセットされるため、この依存関係を解除または削除するだけで済みます。では、ボタンとInheritedWidgetの依存関係を削除するにはどうすればよいでしょうか? 冒頭InheritedWidgetですでに述べました:と の呼び出しの違いは、前者は依存関係を登録するのに対し、後者は依存関係を登録しないことですdependOnInheritedWidgetOfExactType()getElementForInheritedWidgetOfExactType()ChangeNotifierProvider.ofしたがって、実装を次のように変更するだけです。

//添加一个listen参数,表示是否建立依赖关系
static T of<T>(BuildContext context, {
    
    bool listen = true}) {
    
     
   final provider = listen
       ? context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>()
       : context.getElementForInheritedWidgetOfExactType<InheritedProvider<T>>()?.widget
           as InheritedProvider<T>;
   return provider.data;
 }

次に、コードの呼び出し部分を次のように変更します。

Column(
    children: <Widget>[
      Consumer<CartModel>(
        builder: (BuildContext context, cart) =>Text("总价: ${
      
      cart.totalPrice}"),
      ),
      Builder(builder: (context) {
    
    
        print("ElevatedButton build");
        return ElevatedButton(
          child: Text("添加商品"),
          onPressed: () {
    
    
            // listen 设为false,不建立依赖关系
            ChangeNotifierProvider.of<CartModel>(context, listen: false)
                .add(Item(20.0, 1));
          },
        );
      })
    ],
  )

ElevatedButton build変更後に上記の例を再度実行すると、[製品の追加] ボタンをクリックした後、コンソールに " " が出力されなくなり、ボタンが再構築されないことがわかります。合計価格は引き続き更新されます。これは、でConsumer呼び出されたChangeNotifierProvider.ofときのlisten値がデフォルト値であるtrueため、依存関係が確立されたままになります。

以下は、上記の例を最適化した後の完全なコードです。

import 'dart:collection';

import 'package:flutter/material.dart';

// 一个通用的InheritedWidget,保存任需要跨组件共享的状态
class InheritedProvider<T> extends InheritedWidget {
    
    
  const InheritedProvider({
    
    Key? key, required this.data, required Widget child})
      : super(key: key, child: child);

  //共享状态使用泛型
  final T data;

  
  bool updateShouldNotify(InheritedProvider<T> old) {
    
    
    //在此简单返回true,则每次更新都会调用依赖其的子孙节点的`didChangeDependencies`。
    return true;
  }
}

class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget {
    
    
  const ChangeNotifierProvider({
    
    
    Key? key,
    required this.data,
    required this.child,
  }) : super(key: key);

  final Widget child;
  final T data;

  //定义一个便捷方法,方便子树中的widget获取共享数据
  static T of<T>(BuildContext context, {
    
    bool listen = true}) {
    
    
    final provider = listen
        ? context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>()
        : context
            .getElementForInheritedWidgetOfExactType<InheritedProvider<T>>()
            ?.widget as InheritedProvider<T>;
    return provider!.data;
  }

  
  State createState() => _ChangeNotifierProviderState<T>();
}

class _ChangeNotifierProviderState<T extends ChangeNotifier>
    extends State<ChangeNotifierProvider<T>> {
    
    
  void update() {
    
    
    //如果数据发生变化(model类调用了notifyListeners),重新构建InheritedProvider
    setState(() => {
    
    });
  }

  
  void didUpdateWidget(ChangeNotifierProvider<T> oldWidget) {
    
    
    //当Provider更新时,如果新旧数据不"==",则解绑旧数据监听,同时添加新数据监听
    if (widget.data != oldWidget.data) {
    
    
      oldWidget.data.removeListener(update);
      widget.data.addListener(update);
    }
    super.didUpdateWidget(oldWidget);
  }

  
  void initState() {
    
    
    // 给model添加监听器
    widget.data.addListener(update);
    super.initState();
  }

  
  void dispose() {
    
    
    // 移除model的监听器
    widget.data.removeListener(update);
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    
    
    return InheritedProvider<T>(
      data: widget.data,
      child: widget.child,
    );
  }
}

///以上是工具类封装,以下是使用示例 购物车

class Item {
    
    
  Item(this.price, this.count);
  double price; //商品单价
  int count; // 商品份数
}

class CartModel extends ChangeNotifier {
    
    
  // 用于保存购物车中商品列表
  final List<Item> _items = [];

  // 禁止改变购物车里的商品信息
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  // 购物车中商品的总价
  double get totalPrice =>
      _items.fold(0, (value, item) => value + item.count * item.price);

  // 将 [item] 添加到购物车。这是唯一一种能从外部改变购物车的方法。
  void add(Item item) {
    
    
    _items.add(item);
    // 通知监听器(订阅者),重新构建InheritedProvider, 更新状态。
    notifyListeners();
  }
}

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

  
  State createState() => _ProviderRouteState();
}

class _ProviderRouteState extends State<ProviderRoute> {
    
    
  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      appBar: AppBar(
        title: const Text("跨组件状态共享-Provider"),
      ),
      body: Center(
        child: ChangeNotifierProvider<CartModel>(
          data: CartModel(),
          child: Builder(builder: (context) {
    
    
            return Column(
              children: <Widget>[
//                Builder(builder: (context) {
    
    
//                  var cart = ChangeNotifierProvider.of<CartModel>(context);
//                  return Text("总价: ${cart.totalPrice}");
//                  //直接这样其实也可以,但是跨路由的情况下不行
//                  //return Text("总价: ${cartModel.totalPrice}");
//                }),
                //Consumer对应上面的代码封装,更优雅一些
                Consumer<CartModel>(
                  builder: (context, cart) => Text("总价: ${
      
      cart.totalPrice}"),
                ),
                Builder(builder: (context) {
    
    
                  print("ElevatedButton build"); //在后面优化部分会用到
                  return ElevatedButton(
                    child: const Text("添加商品"),
                    onPressed: () {
    
    
                      //给购物车中添加商品,添加后总价会更新 false排除调用者自身也会受影响重新build(因为依赖了InheritedWidget父组件)
                      ChangeNotifierProvider.of<CartModel>(context, listen: false)
                          .add(Item(20.0, 1));
                    },
                  );
                }),
              ],
            );
          }),
        ),
      ),
    );
  }
}

// 这是一个便捷类,会获得当前context和指定数据类型的Provider
class Consumer<T> extends StatelessWidget {
    
    
  const Consumer({
    
    Key? key, required this.builder}) : super(key: key);

  final Widget Function(BuildContext context, T value) builder;

  
  Widget build(BuildContext context) {
    
    
    return builder(context, ChangeNotifierProvider.of<T>(context)); //自动获取Model
  }
}

これまでのところ、pub.dev にプロバイダーProviderのコア機能を備えたミニ バージョンを実装してきましたが、このミニ バージョンには包括的な機能がありません。たとえば、監視可能なバージョンが 1 つだけでありデータ共有だけではありません。再起動時にツリーが常にシングルトンであることを確認する方法など、いくつかの境界が考慮されていませんでした。したがって、実際の戦闘ではプロバイダーパッケージを使用することをお勧めします。ここでのこのミニ実装の主な目的は、プロバイダー パッケージの基礎となる原理を理解することです。ChangeNotifierProviderProviderWidgetbuildModelProvider

プロバイダーパッケージの簡単な使い方

公式の pub.dev で維持されているプロバイダーパッケージの簡単な使用法を見てみましょう。プロバイダー パッケージには、いくつかの一般的なプロバイダーが用意されています。

中国語の文書アドレス:ここをクリックしてください

たとえば、公式プロバイダー パッケージを使用して、ミニ プロバイダーで前の例を実装します。

import 'dart:collection';

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

class Item {
    
    
  Item(this.price, this.count);
  double price; //商品单价
  int count; // 商品份数
}

///如果希望Model发生变化时通知显示的地方更新,必须继承ChangeNotifier,并在改变数据的方法中调用notifyListeners()方法
class CartModel extends ChangeNotifier {
    
    
  // 用于保存购物车中商品列表
  final List<Item> _items = [];

  // 禁止改变购物车里的商品信息
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  // 购物车中商品的总价
  double get totalPrice =>
      _items.fold(0, (value, item) => value + item.count * item.price);

  // 将 [item] 添加到购物车。这是唯一一种能从外部改变购物车的方法。
  void add(Item item) {
    
    
    _items.add(item);
    // 通知监听器(订阅者),重新构建InheritedProvider, 更新状态。
    notifyListeners();
    print("$totalPrice");
  }
}

///使用provider包中提供的ChangeNotifierProvider和Provider实现状态共享
class ProviderRoute2 extends StatelessWidget {
    
    
  const ProviderRoute2({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return ChangeNotifierProvider(
      //这个地方如果是新建的一定要用create,否则如果是提前创建好的CartModel必须用ChangeNotifierProvider.value方法
      create: (context) => CartModel(),
      child: Scaffold(
        appBar: AppBar(title: const Text("跨组件状态共享-Provider"),),
        body: Column(
          children: <Widget>[
            const Text("使用provider中提供的ChangeNotifierProvider和Provider实现状态共享"),
            // Consumer也是provider包中提供的消费者,可以更方便的获取数据展示
            Consumer<CartModel>(
              builder: (context, cart, child) => Text("总价: ${
      
      cart.totalPrice}"),
            ),
            Builder(builder: (context){
    
    
              print("ElevatedButton build");
              return ElevatedButton(
                child: const Text("添加商品"),
                onPressed: () {
    
    
                  //给购物车中添加商品,添加后总价会更新 false可排除调用者自身也会受影响重新build(因为依赖了InheritedWidget父组件)
                  Provider.of<CartModel>(context, listen: false).add(Item(20.0, 1));
                },
              );
            }),
          ],
        ),
      ),
    );
  }
}

使い方は前のものと似ていますが、注意すべき点がいくつかあります。

  • 新しいオブジェクト インスタンスを公開する:新しく作成したオブジェクトを公開する場合、またはリッスンを開始するときに新しいオブジェクト インスタンスを作成する場合は、名前付きコンストラクターの代わりにプロバイダーのデフォルトコンストラクター使用してください。.value例えば:
Provider(
  create: (_) => MyModel(),
  child: ...
)
  • 時間の経過とともに変化する可能性のある変数からオブジェクトを作成することはお勧めできません。たとえば、次のコードでは、値が変更されてもオブジェクトは更新されません
int count;

Provider(
  create: (_) => MyModel(count),
  child: ...
)
  • 外部によって変更される可能性のある変数をオブジェクトに渡したい場合は、次を使用しますProxyProvider
int count;

ProxyProvider0(
  update: (_, __) => MyModel(count),
  child: ...
)
  • プロバイダーcreateupdateコールバックを使用する場合、コールバック関数はデフォルトで遅延的に呼び出されます。つまり、変数が読み取られると、createおよびupdate関数が呼び出されます。オブジェクト内の一部のロジックを事前計算する場合は、lazyパラメーターを使用してこの動作を無効にすることができます。
MyProvider(
  create: (_) => Something(),
  lazy: false,
)
  • 既存のオブジェクト インスタンスを再利用する:既存のオブジェクト インスタンスを公開する場合は、プロバイダーの名前付きコンストラクターを使用することをお勧めしますデフォルトのコンストラクターは非推奨になりました。これを行わないと、メソッドを呼び出したときにオブジェクトがまだ使用されている可能性があり、解放できなくなります。例えば:.value dispose
MyChangeNotifier variable;

ChangeNotifierProvider.value(
  value: variable,
  child: ...
)

読み取り値

値を読み取る最も簡単な方法は、BuildContext( によって挿入された) の拡張プロパティを使用することですprovider

  • context.watch<T>()の変化をwidget監視できますTprovider
  • context.read<T>()、直接返しますTが、リッスンしません。
  • context.select<T,R>(R cb(T value))を使用すると、上のコンテンツの一部のみに対する変更をwidgetリッスンできます。T

この静的メソッドを使用することもできますProvider.of<T>(context)。これは と同等であり、パラメータ (たとえば)context.watchを渡すと、 と同等になりますlisten: falseProvider.of<T>(context,listen: false)context.read

context.read<T>()このメソッドでは、値が変更されたときに がwidget再構築されず、StatelessWidget.buildおよび内でState.build呼び出すことができないことに注意してください。つまり、これら 2 つのメソッド以外のどこからでも呼び出すことができます。

上記のメソッドは、に渡されたBuildContext関連するものからwidget始まるツリーを検索しwidget、見つかった階層の最も近いTタイプを返しますprovider(見つからない場合はエラーをスローします)。この操作の複雑さはO(1)であり、実際にはコンポーネント ツリー全体を走査するわけではないことに注意してください。

公開された値を読み取る簡単な例を次に示します。

class Home extends StatelessWidget {
    
    
  
  Widget build(BuildContext context) {
    
    
    return Text(
      // Don't forget to pass the type of the object you want to obtain to `watch`!
      context.watch<String>(),
    );
  }
}

これらのメソッドを使用したくない場合は、ConsumerおよびSelectorを使用することもできます。これらは、パフォーマンスの最適化が必要なシナリオや、のレベル以下でwidgetの取得が難しい場合に非常に役立ちます。providerBuildContext

存在しない可能性のあるプロバイダーに依存します

場合によっては、provider存在しないクエリをサポートする必要があるかもしれません。たとえば、provider以外の多くの場所で使用されているパッケージを再利用できますwidget

この時点で、プロバイダーが見つからない場合にエラーが報告されるのを避けるために、対応するcontext.watchと をnull 許容型として宣言できます。context.readT

元のコードが次のとおりであるとします。

context.watch<Model>()

provider見つからない場合にスローされProviderNotFoundException次のように変更されます。

context.watch<Model?>()

クエリ時に一致の検索を試み、provider見つからない場合はnull例外をスローせずに戻ります。

プロバイダー パッケージで一般的に使用されるいくつかのプロバイダー:

名前 関数
プロバイダー 最も基本的なプロバイダー コンポーネントは、任意の値を受け入れ、それを公開します。
マルチプロバイダー 複数のプロバイダーの構成をサポートし、複数のプロバイダーのレイヤーごとのネストを回避できます。
リッスナブルプロバイダー リッスン可能なオブジェクトで使用するための特別なプロバイダー。ListenableProvider はオブジェクトをリッスンし、リスナーが呼び出されたときにオブジェクトに依存するウィジェットを更新します。
ChangeNotifierProvider ChangeNotifier に提供される ListenableProvider 仕様は、必要に応じて ChangeNotifier.dispose を自動的に呼び出します。
プロキシプロバイダー 複数のプロバイダーの値を新しいオブジェクトに集約できます。これは、複数のモデルの依存関係の変換に使用できます。1 つのモデルが別のモデルに依存する場合、写真をアップロードする機能など、写真を最初に画像サーバーにアップロードしてから、リンクをバックグラウンド サーバーに送信します。ProxyProvider、ProxyProvider2、ProxyProvider3 など、複数のバリアントがあります。クラス名の後の数字は、ProxyProvider が依存するプロバイダーの数です。
将来のプロバイダー Future を受け取り、Future の完了時にそれに依存するコンポーネントを更新するようにコンシューマーに通知します。FutureProvider は基本的に、通常の FutureBuilder の単なるラッパーです。ただし、Future が完了すると UI は更新されません。FutureProvider は、更新や変更が行われていないページに適しており、FutureBuilder と同じ機能を備えています。
ValueListenableProvider ValueListenable をリッスンし、ValueListenable.value のみを公開します。
ストリームプロバイダー StreamProvider は基本的に、ストリームをリッスンして現在の最新の値を公開する StreamBuilder のラッパーです。StreamProvider はモデル自体への変更をリッスンせず、ストリーム内の新しいイベントのみをリッスンします。

上記の内容をすべて理解する必要はなく、必要に応じて特定のドキュメントを参照することができます。

利用可能なプロバイダーの詳細については、こちらを参照してください

その他の状態管理パッケージ

現在、Flutter コミュニティには状態管理専用のパッケージが多数ありますが、ここでは比較的スコアの高いパッケージをいくつかリストします。

名前 導入
プロバイダースコープ付きモデル どちらのパッケージも InheritedWidget に基づいており、原理は似ています
戻ってきた これは、Web 開発における React エコロジカル チェーンにおける Redux パッケージの Flutter 実装です。
モブX これは、Web 開発における React エコロジカル チェーンにおける MobX パッケージの Flutter 実装です。
ブロック BLoCパターンのFlutter実装です

色とテーマ

1. カラー文字列を Color オブジェクトに変換する方法

たとえば、Web 開発におけるカラー値は通常、RGB 値である「#dc380d」のような文字列であり、次のメソッドで Color クラスに変換できます。

Color(0xffdc380d); //如果颜色固定可以直接使用整数值
//颜色是一个字符串变量
var c = "dc380d";
Color(int.parse(c,radix:16)|0xFF000000) //通过位运算符将Alpha设置为FF
Color(int.parse(c,radix:16)).withAlpha(255)  //通过方法将Alpha设置为FF

2.色の明るさ

背景色とタイトルをカスタマイズできるナビゲーション バーを実装したいとします。背景色が暗い場合はタイトルを明るい色で表示し、背景色が明るい場合はタイトルを白で表示する必要があるとします。暗色。この機能を実現するには、背景色の明るさを計算し、タイトルの色を動的に決定する必要があります。Color クラスは、値computeLuminance()を返すことができるメソッドを[0-1]提供します。数値が大きいほど、色は明るくなります。それに応じてタイトルの色を動的に決定できます。次に、NavBarナビゲーション バーの簡単な実装を示します:

class NavBar extends StatelessWidget {
    
    
  final String title;
  final Color color; //背景颜色

  NavBar({
    
    
    Key? key,
    required this.color,
    required this.title,
  });

  
  Widget build(BuildContext context) {
    
    
    return Container(
      constraints: BoxConstraints(
        minHeight: 52,
        minWidth: double.infinity,
      ),
      decoration: BoxDecoration(
        color: color,
        boxShadow: [
          //阴影
          BoxShadow(
            color: Colors.black26,
            offset: Offset(0, 3),
            blurRadius: 3,
          ),
        ],
      ),
      child: Text(
        title,
        style: TextStyle(
          fontWeight: FontWeight.bold,
          //根据背景色亮度来确定Title颜色
          color: color.computeLuminance() < 0.5 ? Colors.white : Colors.black,
        ),
      ),
      alignment: Alignment.center,
    );
  }
}

テストコードは次のとおりです。

Column(
  children: <Widget>[
    //背景为蓝色,则title自动为白色
    NavBar(color: Colors.blue, title: "标题"), 
    //背景为白色,则title自动为黑色
    NavBar(color: Colors.white, title: "标题"),
  ]
)

実行結果:
ここに画像の説明を挿入

3. 素材の色

MaterialColorマテリアル デザインで色を実装するクラスで、103 レベルの色のグラデーションが含まれています。MaterialColor色の深さは、 「 」演算子のインデックス値によって表されます[]。有効なインデックスは次のとおりです50,100,200,…,900。数値が大きいほど、色は暗くなります。MaterialColorのデフォルト値は、500インデックスが等しい色です。たとえば、これは次のように定義されたColors.blue事前定義クラス オブジェクトです。MaterialColor

static const MaterialColor blue = MaterialColor(
  _bluePrimaryValue,
  <int, Color>{
    
    
     50: Color(0xFFE3F2FD),
    100: Color(0xFFBBDEFB),
    200: Color(0xFF90CAF9),
    300: Color(0xFF64B5F6),
    400: Color(0xFF42A5F5),
    500: Color(_bluePrimaryValue),
    600: Color(0xFF1E88E5),
    700: Color(0xFF1976D2),
    800: Color(0xFF1565C0),
    900: Color(0xFF0D47A1),
  },
);
static const int _bluePrimaryValue = 0xFF2196F3;

shadeXXに従って特定のインデックスの色を取得できます。Colors.blue.shade50取得されたColors.blue.shade900色の値は水色から濃い青に徐々に変化し、その効果は次の図のようになります。

ここに画像の説明を挿入

テーマ

ThemeThemeDataコンポーネントはマテリアル APP のテーマ データ ( ) を定義できます。マテリアル コンポーネント ライブラリの多くのコンポーネントは、ナビゲーション バーの色、タイトル フォント、アイコン スタイルなどのテーマ データを使用します。はそのサブツリーのスタイル データを共有するためにTheme内部で使用されます。InheritedWidget

1. テーマデータ

ThemeDataこれは、マテリアル コンポーネント ライブラリのテーマ データを保存するために使用されます。マテリアル コンポーネントは、対応する設計仕様に準拠する必要があり、これらの仕様のカスタマイズ可能な部分は で定義されているため、アプリケーションのテーマをカスタマイズできますThemeDataThemeData子コンポーネントでは、Theme.ofメソッドを通じて現在のコンポーネントを取得できますThemeData

注: マテリアル デザイン仕様の一部はカスタマイズできません (ナビゲーション バーの高さなど)。これにはThemeDataカスタマイズ可能な部分のみが含まれます。

ThemeDataいくつかのデータ定義を見てみましょう。

ThemeData({
    
    
  Brightness? brightness, //深色还是浅色
  MaterialColor? primarySwatch, //主题颜色样本,见下面介绍
  Color? primaryColor, //主色,决定导航栏颜色
  Color? cardColor, //卡片颜色
  Color? dividerColor, //分割线颜色
  ButtonThemeData buttonTheme, //按钮主题
  Color dialogBackgroundColor,//对话框背景颜色
  String fontFamily, //文字字体
  TextTheme textTheme,// 字体主题,包括标题、body等文字样式
  IconThemeData iconTheme, // Icon的默认样式
  TargetPlatform platform, //指定平台,应用特定平台控件风格
  ColorScheme? colorScheme,
  ...
})

上記はThemeData属性のほんの一部であり、完全なデータ定義は SDK で表示できます。上記の属性で説明する必要があるのは、primarySwatchそれがテーマ カラーの「サンプル カラー」であり、これを通じて、特定の条件下 (たとえば、指定されていない場合、primaryColorおよび現在のテーマがダーク色ではない場合) で他の属性が生成される可能性があるということです。テーマの場合、指定された色がprimaryColorデフォルトになりprimarySwatch、 などの同様のプロパティも影響を受けますindicatorColorprimarySwatch

2. 例

ルーティング スキニング関数を実装します。

class ThemeTestRoute extends StatefulWidget {
    
    
  
  _ThemeTestRouteState createState() => _ThemeTestRouteState();
}

class _ThemeTestRouteState extends State<ThemeTestRoute> {
    
    
  var _themeColor = Colors.teal; //当前路由主题色

  
  Widget build(BuildContext context) {
    
    
    ThemeData themeData = Theme.of(context);
    return Theme(
      data: ThemeData(
          primarySwatch: _themeColor, //用于导航栏、FloatingActionButton的背景色等
          iconTheme: IconThemeData(color: _themeColor) //用于Icon颜色
      ),
      child: Scaffold(
        appBar: AppBar(title: Text("主题测试")),
        body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            //第一行Icon使用主题中的iconTheme
            Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Icon(Icons.favorite),
                  Icon(Icons.airport_shuttle),
                  Text("  颜色跟随主题")
                ]
            ),
            //为第二行Icon自定义颜色(固定为黑色)
            Theme(
              data: themeData.copyWith(
                iconTheme: themeData.iconTheme.copyWith(
                    color: Colors.black
                ),
              ),
              child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Icon(Icons.favorite),
                    Icon(Icons.airport_shuttle),
                    Text("  颜色固定黑色")
                  ]
              ),
            ),
          ],
        ),
        floatingActionButton: FloatingActionButton(
            onPressed: () =>  //切换主题
                setState(() =>
                _themeColor =
                _themeColor == Colors.teal ? Colors.blue : Colors.teal
                ),
            child: Icon(Icons.palette)
        ),
      ),
    );
  }
}

実行後、図に示すように、右下隅にあるフローティング ボタンをクリックしてテーマを切り替えます。

ここに画像の説明を挿入

注意すべき点は次の 2 つです。

  • グローバル テーマは、コードのTheme2 行目のアイコンに固定色 (黒) を指定することと同じように、ローカル テーマでオーバーライドできます。これは一般的な手法であり、このメソッドはサブツリー テーマをカスタマイズするために Flutter でよく使用されます。 。では、なぜローカル テーマがグローバル テーマをオーバーライドできるのでしょうか? これは主に、widgetテーマ スタイルが で使用される場合、Theme.of(BuildContext context)を通じて取得されるためです。その簡略化されたコードを見てみましょう。
static ThemeData of(BuildContext context, {
    
     bool shadowThemeOnly = false }) {
    
    
   // 简化代码,并非源码  
   return context.dependOnInheritedWidgetOfExactType<_InheritedTheme>().theme.data
}

context.dependOnInheritedWidgetOfExactTypeは、現在の位置からtypeの最初の位置までwidgetツリーを検索しますしたがって、パーツが指定されると、そのサブツリーを検索して最初に見つかったものが指定したものになります_InheritedThemewidgetThemeTheme.of()_InheritedThemeTheme

  • MaterialAppこの例は 1 つのルートのスキンを変更するものですが、アプリケーション全体のスキンを変更したい場合は、プロパティを変更できますtheme

オンデマンドで再構築 (ValueListenableBuilder)

InheritedWidgetwidgetツリー内で上から下へデータを共有する方法を提供しますが、下から上や水平など、データの流れが上から下ではない場面も多くあります。この問題を解決するために、Flutter はValueListenableBuilderデータ ソースを監視する機能を備えたコンポーネントを提供し、データ ソースが変更されると再実行されます。これはbuilder次のように定義されます。

const ValueListenableBuilder({
    
    
  Key? key,
  required this.valueListenable, // 数据源,类型为ValueListenable<T>
  required this.builder, // builder
  this.child,
}
  • valueListenable: タイプは でありValueListenable<T>、リッスン可能なデータ ソースを示します。
  • builder: データ ソースが変更されると、サブコンポーネント ツリーがbuilder再度呼び出されますbuild
  • child:サブコンポーネントツリー全体を毎回再構築します サブコンポーネントツリー内に不変部分があれば、それを の3パラメータとしてbuilder渡すことでコンポーネントのキャッシュを実現できます原理は と同じです3番目も同じです。childchildbuilderbuilderAnimatedBuilderchild

ValueListenableBuilderデータの流れの方向とは関係なく、データソースが変更されればサブコンポーネントツリーを再構築するので、どの方向へのデータ共有も実現できることがわかります

例: カウンタを実装する

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

  
  State<ValueListenableRoute> createState() => _ValueListenableState();
}

class _ValueListenableState extends State<ValueListenableRoute> {
    
    
  // 定义一个ValueNotifier,当数字变化时会通知 ValueListenableBuilder
  final ValueNotifier<int> _counter = ValueNotifier<int>(0);
  static const double textScaleFactor = 1.5;

  
  Widget build(BuildContext context) {
    
    
    // 添加 + 按钮不会触发整个 ValueListenableRoute 组件的 build
    print('build');
    return Scaffold(
      appBar: AppBar(title: Text('ValueListenableBuilder 测试')),
      body: Center(
        child: ValueListenableBuilder<int>(
          builder: (BuildContext context, int value, Widget? child) {
    
    
            // builder 方法只会在 _counter 变化时被调用
            return Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                child!,
                Text('$value 次',textScaleFactor: textScaleFactor),
              ],
            );
          },
          valueListenable: _counter,
          // 当子组件不依赖变化的数据,且子组件收件开销比较大时,指定 child 属性来缓存子组件非常有用
          child: const Text('点击了 ', textScaleFactor: textScaleFactor),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        // 点击后值 +1,触发 ValueListenableBuilder 重新构建
        onPressed: () => _counter.value += 1,
      ),
    );
  }
}

実行後、+ ボタンを 2 回続けてクリックした場合の効果を図に示します。

ここに画像の説明を挿入

コンソールは、ページを開いたときに 1 回だけ開きますbuild。+ ボタンをクリックすると、ValueListenableBuilderサブコンポーネント ツリーのみが再構築されますが、ページ全体は再構築されないbuildため、ログ パネルには「build」が 1 回だけ表示されます。したがって、私たちに提案があります。それは、再構築の範囲を減らすことができるように、依存するデータ ソースのみをできる限り構築することです。つまり、分割の粒度をできるだけ細かくする必要があります。ValueListenableBuilderwidgetValueListenableBuilder

次の 2 つの点に注意してくださいValueListenableBuilder

  1. データの流れの方向に関係なく、あらゆる流れ方向でのデータ共有を実現できます。
  2. 実際には、ValueListenableBuilderパフォーマンスを向上させるために、分割の粒度はできるだけ細かくする必要があります。

非同期 UI 更新 (FutureBuilder、StreamBuilder)

多くの場合、UI を動的に更新するために非同期データに依存します。たとえば、ページを開くときは、最初にインターネットからデータを取得する必要があります。データを取得するプロセス中に、読み込みボックスを表示し、次にデータを取得したときのページ; 別の例として、(ファイル フロー、インターネット データ受信フローなど) の進行状況を表示したい場合がありますStreamもちろん、StatefulWidget上記の機能も十分に実現できます。ただし、実際の開発では UI を更新するために非同期データに依存することが非常に一般的であるため、Flutter はこの機能を迅速に実現するための 2 つのコンポーネントFutureBuilderを特別に提供します。StreamBuilder

フューチャービルダー

FutureBuilder1 つに依存しFutureFuture依存する状態に応じて動的に構築されます。FutureBuilderコンストラクターを見てみましょう。

FutureBuilder({
    
    
  this.future,
  this.initialData,
  required this.builder,
})
  • future:FutureBuilder依存性Future。通常は時間のかかる非同期タスクです。

  • initialData:初期データ、ユーザーがデフォルトのデータを設定します。

  • builder: WidgetBuilder; このビルダーはFuture実行のさまざまな段階で複数回呼び出され、ビルダーの署名は次のとおりです。

    Function (BuildContext context, AsyncSnapshot snapshot)

    その中には、snapshot現在の非同期タスクのステータス情報と結果情報が含まれます。たとえば、非同期タスクのsnapshot.connectionStateステータス情報を取得したり、snapshot.hasError非同期タスクにエラーがあるかどうかを判断したりできます。完全な定義は次のようになります。AsyncSnapshotクラス定義で表示されます。

    また、のFutureBuilder関数builderシグネチャはの関数シグネチャと同じですStreamBuilderbuilder

例:ルートを実装します。ルートを開くと、インターネットからデータを取得します。データを取得するとロード ボックスが表示されます。取得が完了すると、成功した場合は取得したデータが表示され、成功した場合は取得したデータが表示されます。失敗するとエラーが表示されます。

ここでは実際にネットワークにアクセスしてデータをリクエストするわけではありませんが、このプロセスをシミュレートして 3 秒後に文字列を返します。

Future<String> mockNetworkData() async {
    
    
  return Future.delayed(Duration(seconds: 3), () => "我是从互联网上获取的数据");
}

FutureBuilderコードは次のように使用します。

class FutureBuilderExample extends StatelessWidget {
    
    
  const FutureBuilderExample({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      appBar: AppBar(title: const Text("FutureBuilder异步刷新UI"),),
      body: Center(
        child: FutureBuilder<String>(
          future: mockNetworkData(),
          initialData: "正在加载中...", //初始化的默认值,可以不传 
          builder: (BuildContext context, AsyncSnapshot snapshot) {
    
     
            if (snapshot.connectionState == ConnectionState.done) {
    
     // 请求已结束
              if (snapshot.hasError) {
    
     // 请求失败,显示错误
                return Text("Error: ${
      
      snapshot.error}");
              } else {
    
     // 请求成功,显示数据
                return Text("Contents: ${
      
      snapshot.data}");
              }
            } else {
    
     // 请求未结束,显示loading
              return Column(
                children: <Widget>[
                  Text(snapshot.data),
                  const CircularProgressIndicator()
                ],
              );
            }
          },
        ),
      )
    );
  }
  Future<String> mockNetworkData() async {
    
    
    return Future.delayed(const Duration(seconds: 3), () => "我是从互联网上获取的数据");
  }
}

操作結果:
ここに画像の説明を挿入

注: サンプル コードでは、コンポーネントがbuildリクエストを再開始するたびに、毎回がfuture新しいため、実際には通常、いくつかのキャッシュ戦略があり、一般的な処理方法はfuture成功後にキャッシュするfutureことで、次回buildは非同期タスクは再開始されません。

上記のコードでは、builder現在の非同期タスクのステータスに応じてConnectionState異なるものを返しますwidgetConnectionStateは、次のように定義された列挙クラスです。

enum ConnectionState {
    
     
  none,     // 当前没有异步任务,比如[FutureBuilder]的[future]为null时  
  waiting, // 异步任务处于等待状态 
  active,  // Stream处于激活状态(流上已经有数据传递了),对于FutureBuilder没有该状态。 
  done,    // 异步任务已经终止.
}

ConnectionState.activeにのみ表示されることに注意してくださいStreamBuilder

ストリームビルダー

Dart では、Stream非同期イベント データの受信にも使用されることがわかっています。Future違いは、複数の非同期操作の結果を受信できることです。ネットワーク コンテンツのダウンロード、ファイルなど、データを複数回読み取る非同期タスクのシナリオでよく使用されます。読み書きなど ストリーム上のイベント(データ)の変化を表示するためのStreamBuilderUIコンポーネントです。Stream

StreamBuilderデフォルトのコンストラクターを見てみましょう。

StreamBuilder({
    
    
  this.initialData,
  Stream<T> stream,
  required this.builder,
}) 

FutureBuilderと のコンストラクターには違いが 1 つだけあることがわかります。前者にはコンストラクターが 1 つ必要futureですが、後者にはコンストラクターが 1 つ必要ですstream

例: 次のコードは、StreamBuilder現在時刻のリアルタイム表示を使用します。

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

class StreamBuilderExample extends StatelessWidget {
    
    
  const StreamBuilderExample({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      appBar: AppBar(
        title: const Text("StreamBuilder异步刷新UI"),
      ),
      body: Center(
        child: StreamBuilder<String>(
          stream: counter(), //
          //initialData: ,// a Stream<int> or null
          builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
    
    
            if (snapshot.hasError) return Text('Error: ${
      
      snapshot.error}');
            switch (snapshot.connectionState) {
    
    
              case ConnectionState.none:
                return const Text('没有Stream');
              case ConnectionState.waiting:
                return const Text('等待数据...');
              case ConnectionState.active:
                return Text('${
      
      snapshot.data}');
              case ConnectionState.done:
                return const Text('Stream已关闭');
            }
          },
        ),
      ),
    );
  }
 
  Stream<String> counter() {
    
    
    return Stream.periodic(const Duration(seconds: 1), (count) {
    
    
      return DateFormat("HH:mm:ss").format(DateTime.now()); // 每隔1s返回当前时间
    });
  }
}

操作結果:

ここに画像の説明を挿入

次のコードは、StreamBuildercombin を使用したStreamController反例を実装しています。

import 'dart:async';
import 'package:flutter/material.dart';
class CustomStreamBuilder extends StatefulWidget {
    
    
  
  _CustomStreamBuilderState createState() => _CustomStreamBuilderState();
}

class _CustomStreamBuilderState extends State<CustomStreamBuilder> {
    
    
  CountGenerator _generator = CountGenerator()..increment();

  
  void dispose() {
    
    
    _generator.dispose(); //关闭控制器
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    
    
    return Container(
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          FlatButton(
            color: Colors.blue,
            shape: CircleBorder(
              side: BorderSide(width: 2.0, color: Color(0xFFFFDFDFDF)),
            ),
            child: Icon(
              Icons.add,
              color: Colors.white,
            ),
            onPressed: () async {
    
    
              await _generator.increment();
            },
          ),
          _buildStreamBuilder(),
          FlatButton(
            color: Colors.blue,
            shape: CircleBorder(
              side: BorderSide(width: 2.0, color: Color(0xFFFFDFDFDF)),
            ),
            child: Icon(
              Icons.remove,
              color: Colors.white,
            ),
            onPressed: () async {
    
    
              await _generator.minus();
            },
          ),
        ],
      ),
    );
  }

  Widget _buildStreamBuilder() => StreamBuilder<int>(
      stream: _generator.state,
      builder: (BuildContext context, AsyncSnapshot snap) {
    
    
        print(snap);
        if (snap.connectionState == ConnectionState.done) {
    
    
          return Text('Done');
        }
        if (snap.connectionState == ConnectionState.active) {
    
    
          return Text(
            snap.data.toString(),
            style: Theme.of(context).textTheme.display1,
          );
        }
        if (snap.connectionState == ConnectionState.waiting) {
    
    
          return CircularProgressIndicator();
        }
        if (snap.hasError) {
    
    
          return Text('Error');
        }
        return Container();
      });
}

class CountGenerator {
    
    
  int _count = 0; //计数器数据
  final StreamController<int> _controller = StreamController(); //控制器

  Stream<int> get state => _controller.stream; //获取状态流
  int get count => _count; //获取计数器数据

  void dispose() {
    
    //关闭控制器
    _controller.close();
  }

  Future<void> increment() async {
    
    //增加记数方法
    _controller.add(++_count);
  }

  Future<void> minus() async {
    
    //增加记数方法
    _controller.add(--_count);
  }
}

注意:StreamController使用しないときは必ずバッテリーの電源を切ってくださいdispose()

DartStreamには、データのフィルタリングに使用できる演算子もいくつか用意されています。次に例を示します。

StreamBuilder(
      stream: _controller.stream
          .where((event) => event > 3)
          .map((event) => event*2)
          .distinct(), // 去除重复的数据
      builder: (context, snapshot) {
    
     
        if (snapshot.connectionState == ConnectionState.done) {
    
    
          return const Text("数据流已关闭");
        }
        if (snapshot.hasError) return Text("${
      
      snapshot.error}");
        if (snapshot.hasData) return Text("${
      
      snapshot.data}");
        return const Center(
          child: CircularProgressIndicator(),
        );
      },
    )

さらに、すでに何らかのFutureオブジェクトを持っていてそれを使用したい場合は、StreamBuilder次のようにFutureオブジェクトをオブジェクトに変換できますStream

Futureシーケンスを次のように変換しますStream

Stream<T> streamFromFutures<T>(Iterable<Future<T>> futures) async* {
    
    
  for (final future in futures) {
    
    
    var result = await future;
    yield result;
  }
}

Futureに変換する別の方法は、次のメソッドStreamを使用することですStream.fromFutures()

 Stream.fromFutures([
    // 1秒后返回结果
    Future.delayed(Duration(seconds: 1), () {
    
    
      return "hello 1";
    }),
    // 抛出一个异常
    Future.delayed(Duration(seconds: 2),(){
    
    
      throw AssertionError("Error");
    }),
    // 3秒后返回结果
    Future.delayed(Duration(seconds: 3), () {
    
    
      return "hello 3";
    })
  ])

ダイアログ

アラートダイアログ

次にAlertDialogマテリアルライブラリのコンポーネントを中心に紹介しますが、そのコンストラクタは次のように定義されています。

const AlertDialog({
    
    
  Key? key,
  this.title, //对话框标题组件
  this.titlePadding, // 标题填充
  this.titleTextStyle, //标题文本样式
  this.content, // 对话框内容组件
  this.contentPadding = const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0), //内容的填充
  this.contentTextStyle,// 内容文本样式
  this.actions, // 对话框操作按钮组
  this.backgroundColor, // 对话框背景色
  this.elevation,// 对话框的阴影
  this.semanticLabel, //对话框语义化标签(用于读屏软件)
  this.shape, // 对话框外形
})

ファイルを削除するときに確認ダイアログ ボックスをポップアップ表示する場合の例を以下に見てみましょう。

ここに画像の説明を挿入

実装コードは次のとおりです。

AlertDialog(
  title: Text("提示"),
  content: Text("您确定要删除当前文件吗?"),
  actions: <Widget>[
    TextButton(
      child: Text("取消"),
      onPressed: () => Navigator.of(context).pop(), //关闭对话框
    ),
    TextButton(
      child: Text("删除"),
      onPressed: () {
    
    
        // ... 执行删除操作
        Navigator.of(context).pop(true); //关闭对话框
      },
    ),
  ],
);

メソッドを通じてダイアログ ボックスを閉じることに注意してくださいNavigator.of(context).pop(…)。これはルートが返す方法と一致しており、どちらも結果データを返すことができます。ダイアログ ボックスを作成したので、それをポップアップするにはどうすればよいでしょうか? また、ダイアログ ボックスから返されたデータはどのように受け取ればよいのでしょうか? これらの質問に対する答えはすべてshowDialog()メソッドの中にあります。

showDialog()マテリアル コンポーネント ライブラリが提供する、マテリアル スタイルのダイアログ ボックスをポップアップするメソッドです。シグネチャは次のとおりです。

Future<T?> showDialog<T>({
    
    
  required BuildContext context,
  required WidgetBuilder builder, // 对话框UI的builder
  bool barrierDismissible = true, //点击对话框barrier(遮罩)时是否关闭它
})

このメソッドはFuture、ダイアログ ボックスの戻り値を受け取るために使用される値を返します: ダイアログ マスクをクリックしてダイアログ ボックスを閉じた場合、値は です。それ以外の場合は、返さFuturenull値です。例全体を見てみましょう。下:Navigator.of(context).pop(result)result

// 点击该按钮后弹出对话框
ElevatedButton(
  child: Text("对话框1"),
  onPressed: () async {
    
    
    // 弹出对话框并等待其关闭
    bool? delete = await showDeleteConfirmDialog1();
    if (delete == null) {
    
    
      print("取消删除");
    } else {
    
    
      print("已确认删除");
      //... 删除文件
    }
  },
),

// 弹出对话框
Future<bool?> showDeleteConfirmDialog1() {
    
    
  return showDialog<bool>(
    context: context,
    builder: (context) {
    
    
      return AlertDialog(
        title: Text("提示"),
        content: Text("您确定要删除当前文件吗?"),
        actions: <Widget>[
          TextButton(
            child: Text("取消"),
            onPressed: () => Navigator.of(context).pop(), // 关闭对话框
          ),
          TextButton(
            child: Text("删除"),
            onPressed: () {
    
     
              Navigator.of(context).pop(true);  // 关闭对话框并返回true
            },
          ),
        ],
      );
    },
  );
}

サンプル実行後、「キャンセル」ボタンまたはダイアログボックスのマスクをクリックするとコンソールに「削除キャンセル」と出力され、「削除」ボタンをクリックすると「削除を確認しました」とコンソールに出力されます。

注:AlertDialogダイアログ ボックスの内容が長すぎると、内容がオーバーフローし、多くの場合、期待どおりにならない可能性があるため、ダイアログ ボックスの内容が長すぎる場合は、内容で折り返すことができますSingleChildScrollView

シンプルダイアログ

SimpleDialogこれはマテリアル コンポーネント ライブラリによって提供されるダイアログでもあり、リスト選択シナリオのリストが表示されます。

以下は APP 言語を選択する例であり、実行結果を図に示します。

ここに画像の説明を挿入

実装コードは次のとおりです。

Future<void> changeLanguage() async {
    
    
  int? i = await showDialog<int>(
      context: context,
      builder: (BuildContext context) {
    
    
        return SimpleDialog(
          title: const Text('请选择语言'),
          children: <Widget>[
            SimpleDialogOption(
              onPressed: () {
    
     
                Navigator.pop(context, 1);  // 返回1
              },
              child: Padding(
                padding: const EdgeInsets.symmetric(vertical: 6),
                child: const Text('中文简体'),
              ),
            ),
            SimpleDialogOption(
              onPressed: () {
    
     
                Navigator.pop(context, 2);  // 返回2
              },
              child: Padding(
                padding: const EdgeInsets.symmetric(vertical: 6),
                child: const Text('美国英语'),
              ),
            ),
          ],
        );
      });

  if (i != null) {
    
    
    print("选择了:${i == 1 ? "中文简体" : "美国英语"}");
  }
}

コンポーネントを使用してリスト項目コンポーネントをラップします。これは、ボタンのテキストが左揃えで小さくなることを除いて、SimpleDialogOptionone と同等です上記の例を実行した後、ユーザーが言語を選択すると、その言語がコンソールに出力されます。TextButtonpadding

ダイアログ

実際には両方AlertDialogともクラスSimpleDialogを使用しますDialogAlertDialogは、サブコンポーネントの実際のサイズを通じて独自のサイズを調整するためにSimpleDialog使用されるIntrinsicWidthため、そのサブコンポーネントは遅延読み込みモデル ( etc などListView、GridView 、 CustomScrollView) のコンポーネントにできなくなります。たとえば、次のコードは、実行後のエラー。

AlertDialog(
  content: ListView(
    children: ...//省略
  ),
);

ネストする必要がある場合はListViewどうすればよいでしょうか? 現時点では、次Dialogのようにクラスを直接使用できます。

Dialog(
  child: ListView(
    children: ...//省略
  ),
);

30 個のリスト項目を含むダイアログ ボックスをポップアップ表示する例を見てみましょう。実行時の効果を図に示します。

ここに画像の説明を挿入

実装コードは次のとおりです。

Future<void> showListDialog() async {
    
    
  int? index = await showDialog<int>(
    context: context,
    builder: (BuildContext context) {
    
    
      var child = Column(
        children: <Widget>[
          ListTile(title: Text("请选择")),
          Expanded(
              child: ListView.builder(
		            itemCount: 30,
		            itemBuilder: (BuildContext context, int index) {
    
    
		              return ListTile(
		                title: Text("$index"),
		                onTap: () => Navigator.of(context).pop(index),
		              );
		            },
          )),
        ],
      );
      // 使用AlertDialog会报错
      // return AlertDialog(content: child);
      return Dialog(child: child);
    },
  );
  if (index != null) {
    
    
    print("点击了:$index");
  }
}

さて、これについてAlertDialog、SimpleDialogも説明しましたDialog上の例では、 を呼び出すとshowDialog、これら 3 つのダイアログ コンポーネントのうちの 1 つが で返されるとbuilder考える人もいるかもしれませんが、これは必要ありません。の例をbuilder考えてみましょう。これを次のコードに完全に置き換えることができますDialogDialog

// return Dialog(child: child) 
return UnconstrainedBox(
  constrainedAxis: Axis.vertical,
  child: ConstrainedBox(
    constraints: BoxConstraints(maxWidth: 280),
    child: Material(
      child: child,
      type: MaterialType.card,
    ),
  ),
);

上記のコードは、実行後に同じ効果を得ることができます。AlertDialog、SimpleDialog次にDialog、マテリアル コンポーネント ライブラリによって提供される 3 つのダイアログ ボックスについて説明します。これらは、開発者がマテリアル デザインの仕様を満たすダイアログ ボックスを迅速に構築できるように設計されていますが、読者はダイアログ ボックスのスタイルを完全にカスタマイズできるため、引き続きさまざまなダイアログを実装できますダイアログ ボックスにはいくつかのスタイルがあり、使いやすさだけでなく、拡張性も優れています。

ダイアログアニメーションとマスク

ダイアログ ボックスは、内部スタイルと外部スタイルの 2 つの部分に分割できます。内部スタイルとは上で紹介したダイアログボックスに表示される具体的な内容を指し、外部スタイルにはダイアログボックスのマスクスタイルやオープニングアニメーションなどが含まれます。

showDialogマテリアル コンポーネント ライブラリで提供されている、マテリアル スタイル ダイアログ ボックスを開くメソッドについてはすでに紹介しました。では、通常のスタイル ダイアログ ボックス (マテリアル スタイル以外) を開くにはどうすればよいでしょうか? Flutter は、showGeneralDialog次のシグネチャを持つメソッドを提供します。

Future<T?> showGeneralDialog<T>({
    
    
  required BuildContext context,
  required RoutePageBuilder pageBuilder, //构建对话框内部UI
  bool barrierDismissible = false, //点击遮罩是否关闭对话框
  String? barrierLabel, // 语义化标签(用于读屏软件)
  Color barrierColor = const Color(0x80000000), // 遮罩颜色
  Duration transitionDuration = const Duration(milliseconds: 200), // 对话框打开/关闭的动画时长
  RouteTransitionsBuilder? transitionBuilder, // 对话框打开/关闭的动画
  ...
})

実際、showDialogこのメソッドはshowGeneralDialog、[マテリアル スタイル] ダイアログ ボックスのマスク カラーとアニメーションをカスタマイズする単なるパッケージです。[マテリアル スタイル] ダイアログ ボックスの開閉アニメーションはFade(フェードインおよびフェードアウト) アニメーションですが、ズーム アニメーションを使用したい場合はカスタマイズできますtransitionBuilder

showCustomDialog次に、ダイアログ ボックスのアニメーションをズーム アニメーションとしてカスタマイズし、同時にマスクの色を次のように指定するメソッドを独自にカプセル化しますColors.black87

Future<T?> showCustomDialog<T>({
    
    
  required BuildContext context,
  bool barrierDismissible = true,
  required WidgetBuilder builder,
  ThemeData? theme,
}) {
    
    
  final ThemeData theme = Theme.of(context, shadowThemeOnly: true);
  return showGeneralDialog(
    context: context,
    pageBuilder: (BuildContext buildContext, Animation<double> animation,
        Animation<double> secondaryAnimation) {
    
    
      final Widget pageChild = Builder(builder: builder);
      return SafeArea(
        child: Builder(builder: (BuildContext context) {
    
    
          return theme != null
              ? Theme(data: theme, child: pageChild)
              : pageChild;
        }),
      );
    },
    barrierDismissible: barrierDismissible,
    barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
    barrierColor: Colors.black87, // 自定义遮罩颜色
    transitionDuration: const Duration(milliseconds: 150),
    transitionBuilder: _buildMaterialDialogTransitions,
  );
}

Widget _buildMaterialDialogTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child) {
    
    
  // 使用缩放动画
  return ScaleTransition(
    scale: CurvedAnimation(
      parent: animation,
      curve: Curves.easeOut,
    ),
    child: child,
  );
}

ここで、開いているファイルの削除確認ダイアログを使用しますshowCustomDialog。コードは次のとおりです。

... //省略无关代码
showCustomDialog<bool>(
  context: context,
  builder: (context) {
    
    
    return AlertDialog(
      title: Text("提示"),
      content: Text("您确定要删除当前文件吗?"),
      actions: <Widget>[
        TextButton(
          child: Text("取消"),
          onPressed: () => Navigator.of(context).pop(),
        ),
        TextButton(
          child: Text("删除"),
          onPressed: () {
    
    
            // 执行删除操作
            Navigator.of(context).pop(true);
          },
        ),
      ],
    );
  },
);

実行結果:

ここに画像の説明を挿入

showDialogマスクの色は、このメソッドで開いたダイアログ ボックスよりも暗いことがわかります。また、ダイアログ ボックスの開閉アニメーションはズーム アニメーションになっており、サンプルを実行して効果を確認できます。

ダイアログの実施原則

showGeneralDialogこのメソッドを例として取り上げ、その具体的な実装を見てみましょう。

Future<T?> showGeneralDialog<T extends Object?>({
    
    
  required BuildContext context,
  required RoutePageBuilder pageBuilder,
  bool barrierDismissible = false,
  String? barrierLabel,
  Color barrierColor = const Color(0x80000000),
  Duration transitionDuration = const Duration(milliseconds: 200),
  RouteTransitionsBuilder? transitionBuilder,
  bool useRootNavigator = true,
  RouteSettings? routeSettings,
}) {
    
    
  return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(RawDialogRoute<T>(
    pageBuilder: pageBuilder,
    barrierDismissible: barrierDismissible,
    barrierLabel: barrierLabel,
    barrierColor: barrierColor,
    transitionDuration: transitionDuration,
    transitionBuilder: transitionBuilder,
    settings: routeSettings,
  ));
}

Navigator実装は非常に単純で、呼び出されたメソッドはpush新しいダイアログ ルート を直接開きRawDialogRoutepush戻り値を返します。ダイアログ ボックスが実際には routing の形式で実装されていることがわかります。これが、 メソッドを使用してダイアログ ボックスを終了できる理由Navigatorですpopダイアログ ボックスのスタイルのカスタマイズに関してRawDialogRoute新しいことは何もありません。自分で確認できます。

ダイアログの状態管理

ユーザーがファイルの削除を選択した場合は、そのファイルを削除するかどうかを尋ねられ、ユーザーがフォルダーを選択した場合は、サブフォルダーを削除するかどうかを確認する必要があります。ユーザーがフォルダーを選択したときに、サブディレクトリを削除するかどうかを確認する 2 番目のポップアップ ウィンドウが表示されるのを避けるために、次に示すように、確認ダイアログ ボックスの下部に「サブディレクトリを同時に削除しますか?」チェック ボックスを追加します。図:

ここに画像の説明を挿入

ここで問題が発生します。チェックボックスのチェック状態をどのように管理するかです。従来は、Stateルーティング ページで選択状態を管理し、次のようなコードを記述します。

class _DialogRouteState extends State<DialogRoute> {
    
    
  bool withTree = false; // 复选框选中状态

  
  Widget build(BuildContext context) {
    
    
    return Column(
      children: <Widget>[
        ElevatedButton(
          child: Text("对话框2"),
          onPressed: () async {
    
    
            bool? delete = await showDeleteConfirmDialog2();
            if (delete == null) {
    
    
              print("取消删除");
            } else {
    
    
              print("同时删除子目录: $delete");
            }
          },
        ),
      ],
    );
  }

  Future<bool?> showDeleteConfirmDialog2() {
    
    
    withTree = false; // 默认复选框不选中
    return showDialog<bool>(
      context: context,
      builder: (context) {
    
    
        return AlertDialog(
          title: Text("提示"),
          content: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              Text("您确定要删除当前文件吗?"),
              Row(
                children: <Widget>[
                  Text("同时删除子目录?"),
                  Checkbox(
                    value: withTree,
                    onChanged: (bool value) {
    
     // 复选框选中状态发生变化时重新构建UI 
                      setState(() {
    
     
                        withTree = !withTree; // 更新复选框状态
                      });
                    },
                  ),
                ],
              ),
            ],
          ),
          actions: <Widget>[
            TextButton(
              child: Text("取消"),
              onPressed: () => Navigator.of(context).pop(),
            ),
            TextButton(
              child: Text("删除"),
              onPressed: () {
    
     
                Navigator.of(context).pop(withTree);  // 执行删除操作
              },
            ),
          ],
        );
      },
    );
  }
}

次に、上記のコードを実行すると、チェックボックスがまったくチェックされていないことがわかります。なぜそうなるのでしょうか?

実際、理由は非常に単純です。setStateメソッドが現在のサブツリーに対してのみ再作成されることはわかっていますがcontextbuildダイアログ ボックスはメソッド内に構築されず、個別に構築されるため、メソッド内での呼び出しは、構築された UI に影響を与えることはできません。_DialogRouteStatebuildshowDialog_DialogRouteStatecontextsetStateshowDialog

さらに、この現象を別の角度から理解することもできます。前述したように、ダイアログ ボックスもルーティングを通じて実装されているため、上記のコードは実際には親ルート内で子ルートを呼び出して更新しようとしているのと同じですsetState。道!要するに、根本的な原因はcontext何かが間違っているということです。チェックボックスをクリックできるようにするにはどうすればよいですか? 通常、次の 3 つの方法があります。

1. StatefulWidgetを個別に抽出する

それはcontext間違っているため、直接的なアイデアは、チェック ボックスの選択ロジックを 1 つにカプセル化しStatefulWidget、その中でチェック状態を管理することです。まずこのメソッドを見てみましょう。実装コードは次のとおりです。

// 单独封装一个内部管理选中状态的复选框组件
class DialogCheckbox extends StatefulWidget {
    
    
  DialogCheckbox({
    
     Key? key, this.value, required this.onChanged, });

  final ValueChanged<bool?> onChanged;
  final bool? value;

  
  _DialogCheckboxState createState() => _DialogCheckboxState();
}

class _DialogCheckboxState extends State<DialogCheckbox> {
    
    
  bool? value;

  
  void initState() {
    
    
    value = widget.value;
    super.initState();
  }

  
  Widget build(BuildContext context) {
    
    
    return Checkbox(
      value: value,
      onChanged: (v) {
    
     
        widget.onChanged(v); // 将选中状态通过事件的形式抛出
        setState(() {
    
     
          value = v; // 更新自身选中状态
        });
      },
    );
  }
}

ポップアップ ダイアログのコードは次のとおりです。

Future<bool?> showDeleteConfirmDialog3() {
    
    
  bool _withTree = false; // 记录复选框是否选中
  return showDialog<bool>(
    context: context,
    builder: (context) {
    
    
      return AlertDialog(
        title: Text("提示"),
        content: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Text("您确定要删除当前文件吗?"),
            Row(
              children: <Widget>[
                Text("同时删除子目录?"),
                DialogCheckbox(
                  value: _withTree, // 默认不选中
                  onChanged: (bool value) {
    
     
                    _withTree = !_withTree; // 更新选中状态
                  },
                ),
              ],
            ),
          ],
        ),
        actions: <Widget>[
          TextButton(
            child: Text("取消"),
            onPressed: () => Navigator.of(context).pop(),
          ),
          TextButton(
            child: Text("删除"),
            onPressed: () {
    
     
              Navigator.of(context).pop(_withTree);  // 将选中状态返回
            },
          ),
        ],
      );
    },
  );
}

最後に、次のように使用します。

ElevatedButton(
  child: Text("话框3(复选框可点击)"),
  onPressed: () async {
    
    
    // 弹出删除确认对话框,等待用户确认
    bool? deleteTree = await showDeleteConfirmDialog3();
    if (deleteTree == null) {
    
    
      print("取消删除");
    } else {
    
    
      print("同时删除子目录: $deleteTree");
    }
  },
),

実行後の効果:

ここに画像の説明を挿入

チェックボックスがオンになっていることがわかり、「キャンセル」または「削除」をクリックすると、コンソールに最終確認ステータスが出力されます。

2. StatefulBuilder メソッドを使用する

上記の方法は、ダイアログ ボックスの状態を更新する問題を解決できますが、明らかな欠点があります。状態を変更する可能性のあるダイアログ ボックス上のすべてのコンポーネントは、内部管理状態に個別にカプセル化する必要があり、面倒なだけではありません。しかし、複雑でもありStatefulWidget、あまり役に立ちません。それで、もっと簡単な方法を見つけられるかどうか見てみましょう? 上記の方法は基本的にダイアログ ボックスの状態をコンテキストに入れてStatefulWidget内部で管理するものですが、コンポーネントを個別に抽出せずにコンテキストStatefulWidgetを作成する方法はありますか? StatefulWidgetこのことを考えると、Builderコンポーネントの実装からインスピレーションを得ることができます。前の紹介では、Builderコンポーネントはコンポーネントの実際の位置を取得できますContextが、どのようにしてそれを実現するのでしょうか?そのソース コードを見てみましょう。

class Builder extends StatelessWidget {
    
    
  const Builder({
    
    
    Key? key,
    required this.builder,
  }) : assert(builder != null),
       super(key: key);
  final WidgetBuilder builder;

  
  Widget build(BuildContext context) => builder(context);
}

Builder実際には継承しているだけでStatelessWidgetbuildメソッド内でカレントを取得したcontext後に構築メソッドをコールバックに委譲しており、実際には()のコンテキストを取得しているbuilderことが分かりますでは、同じメソッドを使用してコンテキストを取得し、そのメソッドをプロキシすることはできるでしょうか? 猫の写真を撮り、トラを描いてメソッドをカプセル化しましょう。BuilderStatelessWidgetcontextStatefulWidgetbuildStatefulBuilder

class StatefulBuilder extends StatefulWidget {
    
    
  const StatefulBuilder({
    
    
    Key? key,
    required this.builder,
  }) : assert(builder != null),
       super(key: key);

  final StatefulWidgetBuilder builder;

  
  _StatefulBuilderState createState() => _StatefulBuilderState();
}

class _StatefulBuilderState extends State<StatefulBuilder> {
    
    
  
  Widget build(BuildContext context) => widget.builder(context, setState);
}

コードは非常にシンプルで、StatefulBuilder が StatefulWidget のコンテキストを取得し、その構築プロセスを委任します。次に、StatefulBuilder を使用して上記のコードをリファクタリングできます (変更は DialogCheckbox 部分のみです)。

... //省略无关代码
Row(
  children: <Widget>[
    Text("同时删除子目录?"), 
    StatefulBuilder(  // 使用 StatefulBuilder 来构建 StatefulWidget 上下文
      builder: (context, _setState) {
    
    
        return Checkbox(
          value: _withTree, 
          onChanged: (bool value) {
    
    
            // _setState 方法实际就是该 StatefulWidget 的 setState 方法,调用后 builder 方法会重新被调用 
            _setState(() {
    
     
              _withTree = !_withTree;  // 更新选中状态
            });
          },
        );
      },
    ),
  ],
),

実際、このメソッドは本質的に、子コンポーネントが親コンポーネント(StatefulWidgetbuildに、子コンポーネント自体の更新を通知して UI を更新することを意味しており、コードを比較すると理解できます。実はStatefulBuilderこれは Flutter SDK で提供されているクラスであり、Builderや と同じ原理であり、StatefulBuilderFlutterBuilderでは非常に実践的なため、しっかり理解する必要があります。

3. 独創的な解決策

もっと簡単な解決策はありますか? この問題を確認するには、まず UI がどのように更新されるかを理解する必要があります。setStateメソッドを呼び出した後にStatefulWidget再起動されることがわかっていますbuildsetStateこのメソッドは何を行うのでしょうか? そこから抜け出す方法は見つかるでしょうか?この考え方に従って、setStateコアのソース コードを確認する必要があります。

void setState(VoidCallback fn) {
    
    
  ... //省略无关代码
  _element.markNeedsBuild();
}

setState前に述べたElementように、Flutter は応答性の高いフレームワークであるため、メソッドが呼び出されたことがわかります。UI をmarkNeedsBuild()更新するには、状態を変更し、ページをリファクタリングする必要があることをフレームワークに通知するだけです。メソッドは次のとおりElementですmarkNeedsBuild()。この機能を実現します!markNeedsBuild()メソッドは現在のオブジェクトを " " (ダーティ) としてElementマークしdirty、毎回Flutter は " "Frameとマークされたオブジェクトを再構築しますdirtyElement

その場合、Elementダイアログ ボックス内の UI オブジェクトを取得し、それを「dirty」としてマークする方法はありますか? 答えは「はい」です!Contextオブジェクトは によって取得できます。これは実際にはElementコンポーネント ツリー内のオブジェクトへの参照ですこれを理解したら、ソリューションを提供する準備が整いました。次の方法でチェック ボックスを更新可能にできます。contextElement

Future<bool?> showDeleteConfirmDialog4() {
    
    
  bool _withTree = false;
  return showDialog<bool>(
    context: context,
    builder: (context) {
    
    
      return AlertDialog(
        title: Text("提示"),
        content: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Text("您确定要删除当前文件吗?"),
            Row(
              children: <Widget>[
                Text("同时删除子目录?"),
                Checkbox( // 依然使用Checkbox组件
                  value: _withTree,
                  onChanged: (bool value) {
    
    
                    // 此时 context 为对话框UI的根 Element,我们直接将对话框UI对应的 Element 标记为 dirty
                    (context as Element).markNeedsBuild();
                    _withTree = !_withTree;
                  },
                ),
              ],
            ),
          ],
        ),
        actions: <Widget>[
          TextButton(
            child: Text("取消"),
            onPressed: () => Navigator.of(context).pop(),
          ),
          TextButton(
            child: Text("删除"),
            onPressed: () {
    
     
              Navigator.of(context).pop(_withTree); // 执行删除操作
            },
          ),
        ],
      );
    },
  );
}

上記のコードを実行すると、チェックボックスも正常に選択できるようになります。ご覧のとおり、たった 1 行のコードでこの問題を解決しました。もちろん、上記のコードは最適ではありません。更新する必要があるのはチェック ボックスの状態のみであり、現時点ではダイアログ ボックスのルートをcontext使用しているため、ダイアログ ボックス全体のすべての UI コンポーネントが更新されます。したがって、最善の方法は次のとおりです。 の「スコープ」が狭められ、つまり としてマークのみが付けられ、最適化されたコードは次のようになります。contextrebuildcontextCheckboxElementdirty

... //省略无关代码
Row(
  children: <Widget>[
    Text("同时删除子目录?"),
    // 通过 Builder 来获得构建 Checkbox 的 `context`, 这是一种常用的缩小 `context` 范围的方式
    Builder(
      builder: (BuildContext context) {
    
    
        return Checkbox(
          value: _withTree,
          onChanged: (bool value) {
    
    
            (context as Element).markNeedsBuild();
            _withTree = !_withTree;
          },
        );
      },
    ),
  ],
),

context指定したターゲットに絞り込む方法は次のとおりですWidgetを介してBuilderビルド ターゲットWidgetを取得しますcontext

他の種類のダイアログ

1. 下部メニューリスト

showModalBottomSheetメソッドは、次のようにマテリアル スタイルのボトム メニュー リスト モーダル ダイアログ ボックスをポップアップできます。

// 弹出底部菜单列表模态对话框
Future<int?> _showModalBottomSheet() {
    
    
  return showModalBottomSheet<int>(
    context: context,
    builder: (BuildContext context) {
    
    
      return ListView.builder(
        itemCount: 30,
        itemBuilder: (BuildContext context, int index) {
    
    
          return ListTile(
            title: Text("$index"),
            onTap: () => Navigator.of(context).pop(index),
          );
        },
      );
    },
  );
}

ボタンをクリックしてダイアログ ボックスをポップアップ表示します。

ElevatedButton(
  child: Text("显示底部菜单列表"),
  onPressed: () async {
    
    
    int type = await _showModalBottomSheet();
    print(type);
  },
),

実行後の効果:

ここに画像の説明を挿入

2. ローディングボックス

実際、LoadingボックスはshowDialog+AlertDialog次の方法で直接カスタマイズできます。

showLoadingDialog() {
    
    
  showDialog(
    context: context,
    barrierDismissible: false, //点击遮罩不关闭对话框
    builder: (context) {
    
    
      return AlertDialog(
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            CircularProgressIndicator(),
            Padding(
              padding: const EdgeInsets.only(top: 26.0),
              child: Text("正在加载,请稍后..."),
            )
          ],
        ),
      );
    },
  );
}

表示効果:

ここに画像の説明を挿入

ボックスの幅が広すぎると思われLoading、ダイアログ ボックスの幅をカスタマイズしたい場合は、ダイアログ ボックスに最小幅の制約が設定されているため、現時点ではそれを使用するSizedBoxかどうしか選択できません。 「サイズ制限のレイアウト」セクションで述べたように、最初に幅の制約をオフセットしてから、指定された幅を使用することができます。コードは次のとおりです。ConstrainedBoxshowDialogUnconstrainedBoxshowDialogSizedBox

... //省略无关代码
UnconstrainedBox(
  constrainedAxis: Axis.vertical,
  child: SizedBox(
    width: 280,
    child: AlertDialog(
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          CircularProgressIndicator(value: .8,),
          Padding(
            padding: const EdgeInsets.only(top: 26.0),
            child: Text("正在加载,请稍后..."),
          )
        ],
      ),
    ),
  ),
);

コード実行の効果:

ここに画像の説明を挿入

3. カレンダーピッカー

マテリアル スタイルのカレンダー セレクターを見てみましょう。

ここに画像の説明を挿入

実装コード:

Future<DateTime?> _showDatePicker1() {
    
    
  var date = DateTime.now();
  return showDatePicker(
    context: context,
    initialDate: date,
    firstDate: date,
    lastDate: date.add( //未来30天可选
      Duration(days: 30),
    ),
  );
}

iOS スタイルのカレンダー ピッカーは、showCupertinoModalPopupメソッドとCupertinoDatePickerコンポーネントを使用して実装する必要があります。

Future<DateTime?> _showDatePicker2() {
    
    
  var date = DateTime.now();
  return showCupertinoModalPopup(
    context: context,
    builder: (ctx) {
    
    
      return SizedBox(
        height: 200,
        child: CupertinoDatePicker(
          mode: CupertinoDatePickerMode.dateAndTime,
          minimumDate: date,
          maximumDate: date.add(
            Duration(days: 30),
          ),
          maximumYear: date.year + 1,
          onDateTimeChanged: (DateTime value) {
    
    
            print(value);
          },
        ),
      );
    },
  );
}

実行結果:

ここに画像の説明を挿入

システムの組み込み効果に満足できない場合は、スタイルを統一し、国際化をサポートするflutter_datetime_picker_plusライブラリの使用を検討できます。

アダプティブダイアログ

上記で紹介した多くのポップアップ ウィンドウについて、オペレーティング プラットフォームに応じて異なるプラットフォーム スタイルのポップアップ ウィンドウ (カレンダーなど) を表示したい場合、場合によっては 2 つの UI セット間の互換性を持たせる必要があることがわかりました。コンポーネント、マテリアルとクパチーノ。これは、2 セットのコードを記述することを意味します。

呼び出しメソッドを 1 つだけ使用し、異なるプラットフォームで実行するときに異なるプラットフォーム スタイルでダイアログ ボックスを表示したい場合は、 pub.dev のadaptive_dialogライブラリの使用を検討できます。

たとえば、ダイアログ ボックスをキャンセルすることを確認した場合、API を呼び出すだけで、Android と iOS 上のそれぞれのシステムの影響が表示されます。

ここに画像の説明を挿入

詳しい使い方は公式ドキュメントを参照してください。

トースト

Toast はポップアップ ウィンドウではありませんが、ポップアップ コンポーネントでもあります。Android のネイティブ Toast 効果に似ています。Flutter SDK には同様のコンポーネントがありません。パブで人気のあるライブラリ fluttertoast の使用を検討できます。コミュニティ

効果の一部を次に示します。

ここに画像の説明を挿入

詳しい使い方は公式ドキュメントを参照してください。


参考:

おすすめ

転載: blog.csdn.net/lyabc123456/article/details/130878576