インターセプトリターンキー (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)
InheritedWidget
widget
これは Flutter の非常に重要な機能コンポーネントです。ツリーの上から下までデータを共有する方法を提供します。たとえば、アプリケーションのルートでデータの一部を共有する場合、それをどの子でもwidget
使用できますそのシェアのデータを取得してください! この機能は、ツリー全体でデータを共有する必要があるシナリオで非常に便利です。たとえば、アプリケーションのテーマと(現在のロケール) 情報はFlutter SDK を通じて共有されます。InheritedWidget
widget
widget
InheritedWidget
Theme
Locale
InheritedWidget
Reactの関数と同様にcontext
、コンポーネントはレベルごとにデータを渡すのではなく、レベル間でデータを受け渡すことができます。ツリーInheritedWidget
内のデータ転送の方向は上から下であり、通知転送の方向とはまったく逆です。widget
Notification
私たちのバージョンの「Counter」サンプル アプリケーションを見てみましょうInheritedWidget
。InheritedWidget
この例は主にカウンタの機能特性を示すためのものであり、カウンタの推奨実装ではないことに注意してください。
まず、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
内でデータを参照するサブコンポーネントを実装します。同時に、コールバックでログを出力します。build
ShareDataWidget
didChangeDependencies()
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
、依存関係がないため呼び出されないことにbuild
ShareDataWidget
didChangeDependencies()
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
_TestWidgetState
didChangeDependencies()
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()
が調整されていることがわかります。dependOnInheritedElement
dependOnInheritedElement
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
が、引き続き呼び出されることに注意してください。この理由は、実際には、「 」ボタンをクリックした後、メソッドが呼び出され、このときにページ全体が再構築されるためです。この例ではキャッシュがないため、キャッシュも再構築されます。とも呼ばれます。_TestWidgetState
didChangeDependencies()
build()
Increment
_InheritedWidgetTestRouteState
setState()
_TestWidget
build()
そこで、ここで問題が発生します。実際、ShareDataWidget
サブツリー内の依存コンポーネントを更新したいだけであり、メソッドが呼び出されている限り_InheritedWidgetTestRouteState
、setState()
すべての子ノードが再作成されますbuild
が、これは不要なので、何か方法はありますか?避けてください?毛織物? 答えはキャッシングです!簡単な方法は、サブツリーをカプセル化してキャッシュすることです(具体的な方法は、 StatefulWidget
WidgetWidget
を通じてそれを実現する方法で後で紹介します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。_inheritedWidgets
Element Tree
T
InheritedElement
ancestor
Element
dependOnInheritedElement
_dependencies
InheritedElement
ancestor
ancestor
updateDependencies
// 代码清单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
トリガーされます。このメソッドは最終的に独自のメソッドを呼び出しますが、その役割自体がエージェントであり、特定のビルドプロセス ロジックがエージェント内にあるため、メソッドはその子を直接返すことがわかります。さらに、のコンストラクターはによって変更され、対応するサブツリーは次のビルドプロセスで直接保持されます。didChangeDependencies
rebuild
build
ProxyElement
build
Widget
Widget
InheritedWidget
const
Element 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 からわかるように、現在のノードのフィールドを としてマークします。 については、そのおよびメソッドのコールバックが順番にトリガーされます。markNeedsBuild
dirty
Element
_didChangeDependencies
true
StatefulElement
didChangeDependencies
build
// 代码清单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 A
Element 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 Widget
InheritedWidget
破棄ロジックに関しては、ノードが削除されるとメソッドがトリガーされ、現在のノードが依存するすべての親ノードのデータ構造Element
からElement Tree
削除されますElement
。deactivate()
Element
Map
// 代码清单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
形式で保存されるか、親ノードから直接継承されます (親ノードに利用可能な依存関係情報がある場合)。Widget
Element
-
更新が必要な場合、メソッドはビルド プロセスでトリガーされます。このメソッドの最終的な呼び出しロジックは、それぞれ
InheritedElement
のthisを走査します。つまり、それに依存するすべての子ノード オブジェクトを取得し、それぞれのメソッドを呼び出します。子ノード。update
_dependents
Map
key
Element
Element
didChangeDependencies
これにより、メソッドがノードを としてマークし、フレームの更新を要求します
StatefulElement
。markNeedsBuild
これdirty
により、最終的にメソッドと、StatefulWidget
対応するState
クラス オブジェクト (StatefulElement
holding)の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();
}
オブザーバー パターンを通じてコンポーネント間の状態共有を実現するには、明らかな欠点がいくつかあることがわかります。
- さまざまなイベントを明示的に定義する必要があり、管理は容易ではありません。
- サブスクライバは状態変更コールバックを明示的に登録する必要があり、メモリ リークを避けるためにコンポーネントが破棄されたときにコールバックのバインドを手動で解除する必要があります。
Flutter でコンポーネント全体の状態を管理するより良い方法はありますか? 答えは「はい」です。では、どうすればよいでしょうか? 先ほど紹介したことを考えてみましょう。その本来の特徴は、子孫コンポーネントとの依存関係をInheritedWidget
バインドできること、そしてデータが変更された場合、依存する子孫コンポーネントを自動的に更新できることです。この機能を使用すると、 のコンポーネント間で共有する必要がある状態を保存し、サブコンポーネントでそれを参照できます。Flutter コミュニティでよく知られているプロバイダーパッケージは、このアイデアに基づいたコンポーネント間の状態共有ソリューションのセットです。 . 次に使い方と原理を詳しく紹介します。InheritedWidget
InheritedWidget
InheritedWidget
InheritedWidget
Provider
プロバイダー
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 つの問題に直面します。
- データの変更を通知するにはどうすればよいですか?
- 誰が再建するのか
InheritedProvider
?
最初の問題は実際には簡単に解決できます。もちろん、イベント通知に以前の導入を使用することもできますが、Flutter 開発にeventBus
近づけるために、Flutter SDK で提供されるクラスを使用します。スタイル パブリッシャー - サブスクライバー モードの場合、定義はおおよそ次のとおりです。ChangeNotifier
Listenable
ChangeNotifier
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()
簡単に取得するためにサブクラスの静的メソッドを定義します。以下では、このクラスの対応するクラスを実装します。Widget
InheritedProvider
_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.child
build
InheritedProvider
child
widget
widget.child
build
child
ChangeNotifierProvider
Widget
build
child
必要なツール クラスがすべて完成したので、ショッピング カートの例を使用して、上記のクラスの使用方法を見てみましょう。
ショッピング カート内のすべての商品の合計価格を表示する関数を実装する必要があります。新しい商品がショッピング カートに追加されると、合計価格が更新されます。
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
、依存する子孫が更新されます。InheritedWidget
Widget
Provider
これを使用すると、次のような利点があることがわかります。
- 私たちのビジネス コードはデータにさらに注意を払っており、データが更新されている限り、
Model
状態が変化した後に手動で呼び出してsetState()
ページを明示的に更新するのではなく、UI は自動的に更新されます。 - データ変更のメッセージ配信はブロックされ、状態変更イベントのパブリケーションとサブスクリプションを手動で処理する必要はありません。これらはすべて にカプセル化されています
Provider
。これは本当に素晴らしいことであり、多くの作業を節約できます。 - 大規模で複雑なアプリケーション、特にグローバルに共有する必要がある状態が多数ある場合、これを使用すると
Provider
コード ロジックが大幅に簡素化され、エラーの可能性が減り、開発効率が向上します。
問題の最適化
上記の実装には、ChangeNotifierProvider
コード構成の問題とパフォーマンスの問題という 2 つの明らかな欠点があります。
1. コード構成の問題
最初に合計価格テキストを作成して表示するコードを見てみましょう。
Builder(builder: (context){
var cart=ChangeNotifierProvider.of<CartModel>(context);
return Text("总价: ${
cart.totalPrice}");
})
このコードは次の 2 つの方法で最適化できます。
ChangeNotifierProvider.of
APP に多くの内部依存関係がある場合、CartModel
そのようなコードは非常に冗長になります。- セマンティクスは明確ではありません。
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
}
}
Consumer
ChangeNotifierProvider.of
実装は非常に単純で、テンプレート パラメーターを指定して対応するものを取得し、自動的に内部で呼び出しますModel
。Consumer
名前自体は正確なセマンティクス (コンシューマー) を持ちます。上記のコード ブロックは次のように最適化できます。
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
つまりBuilder
、ChangeNotifierProvider.of
ツリーWidget
上のInheritedWidget
(ie ) に依存するInheritedProvider
ため、製品が変更があった場合はCartModel
通知されChangeNotifierProvider
、ChangeNotifierProvider
サブツリーが再構築されるため、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 つだけであり、データ共有だけではありません。再起動時にツリーが常にシングルトンであることを確認する方法など、いくつかの境界が考慮されていませんでした。したがって、実際の戦闘ではプロバイダーパッケージを使用することをお勧めします。ここでのこのミニ実装の主な目的は、プロバイダー パッケージの基礎となる原理を理解することです。ChangeNotifierProvider
Provider
Widget
build
Model
Provider
プロバイダーパッケージの簡単な使い方
公式の 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: ...
)
- プロバイダー
create
とupdate
コールバックを使用する場合、コールバック関数はデフォルトで遅延的に呼び出されます。つまり、変数が読み取られると、create
およびupdate
関数が呼び出されます。オブジェクト内の一部のロジックを事前計算する場合は、lazy
パラメーターを使用してこの動作を無効にすることができます。
MyProvider(
create: (_) => Something(),
lazy: false,
)
- 既存のオブジェクト インスタンスを再利用する:既存のオブジェクト インスタンスを公開する場合は、プロバイダーの名前付きコンストラクターを使用することをお勧めします。デフォルトのコンストラクターは非推奨になりました。これを行わないと、メソッドを呼び出したときにオブジェクトがまだ使用されている可能性があり、解放できなくなります。例えば:
.value
dispose
MyChangeNotifier variable;
ChangeNotifierProvider.value(
value: variable,
child: ...
)
読み取り値
値を読み取る最も簡単な方法は、BuildContext
( によって挿入された) の拡張プロパティを使用することですprovider
。
context.watch<T>()
型の変化をwidget
監視できます。T
provider
context.read<T>()
、直接返しますT
が、リッスンしません。context.select<T,R>(R cb(T value))
を使用すると、上のコンテンツの一部のみに対する変更をwidget
リッスンできます。T
この静的メソッドを使用することもできますProvider.of<T>(context)
。これは と同等であり、パラメータ (たとえば)context.watch
を渡すと、 と同等になります。listen: false
Provider.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
の取得が難しい場合に非常に役立ちます。provider
BuildContext
存在しない可能性のあるプロバイダーに依存します
場合によっては、provider
存在しないクエリをサポートする必要があるかもしれません。たとえば、provider
以外の多くの場所で使用されているパッケージを再利用できますwidget
。
この時点で、プロバイダーが見つからない場合にエラーが報告されるのを避けるために、対応するcontext.watch
と をnull 許容型として宣言できます。context.read
T
元のコードが次のとおりであるとします。
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
マテリアル デザインで色を実装するクラスで、10
3 レベルの色のグラデーションが含まれています。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
色の値は水色から濃い青に徐々に変化し、その効果は次の図のようになります。
テーマ
Theme
ThemeData
コンポーネントはマテリアル APP のテーマ データ ( ) を定義できます。マテリアル コンポーネント ライブラリの多くのコンポーネントは、ナビゲーション バーの色、タイトル フォント、アイコン スタイルなどのテーマ データを使用します。はそのサブツリーのスタイル データを共有するためにTheme
内部で使用されます。InheritedWidget
1. テーマデータ
ThemeData
これは、マテリアル コンポーネント ライブラリのテーマ データを保存するために使用されます。マテリアル コンポーネントは、対応する設計仕様に準拠する必要があり、これらの仕様のカスタマイズ可能な部分は で定義されているため、アプリケーションのテーマをカスタマイズできますThemeData
。ThemeData
子コンポーネントでは、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
、 などの同様のプロパティも影響を受けますindicatorColor
。primarySwatch
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 つです。
- グローバル テーマは、コードの
Theme
2 行目のアイコンに固定色 (黒) を指定することと同じように、ローカル テーマでオーバーライドできます。これは一般的な手法であり、このメソッドはサブツリー テーマをカスタマイズするために Flutter でよく使用されます。 。では、なぜローカル テーマがグローバル テーマをオーバーライドできるのでしょうか? これは主に、widget
テーマ スタイルが で使用される場合、Theme.of(BuildContext context)
を通じて取得されるためです。その簡略化されたコードを見てみましょう。
static ThemeData of(BuildContext context, {
bool shadowThemeOnly = false }) {
// 简化代码,并非源码
return context.dependOnInheritedWidgetOfExactType<_InheritedTheme>().theme.data
}
context.dependOnInheritedWidgetOfExactType
は、現在の位置からtypeの最初の位置までwidget
ツリーを検索します。したがって、パーツが指定されると、そのサブツリーを検索して最初に見つかったものが指定したものになります。_InheritedTheme
widget
Theme
Theme.of()
_InheritedTheme
Theme
MaterialApp
この例は 1 つのルートのスキンを変更するものですが、アプリケーション全体のスキンを変更したい場合は、プロパティを変更できますtheme
。
オンデマンドで再構築 (ValueListenableBuilder)
InheritedWidget
widget
ツリー内で上から下へデータを共有する方法を提供しますが、下から上や水平など、データの流れが上から下ではない場面も多くあります。この問題を解決するために、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番目も同じです。child
child
builder
builder
AnimatedBuilder
child
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 回だけ表示されます。したがって、私たちに提案があります。それは、再構築の範囲を減らすことができるように、依存するデータ ソースのみをできる限り構築することです。つまり、分割の粒度をできるだけ細かくする必要があります。ValueListenableBuilder
widget
ValueListenableBuilder
次の 2 つの点に注意してくださいValueListenableBuilder
。
- データの流れの方向に関係なく、あらゆる流れ方向でのデータ共有を実現できます。
- 実際には、
ValueListenableBuilder
パフォーマンスを向上させるために、分割の粒度はできるだけ細かくする必要があります。
非同期 UI 更新 (FutureBuilder、StreamBuilder)
多くの場合、UI を動的に更新するために非同期データに依存します。たとえば、ページを開くときは、最初にインターネットからデータを取得する必要があります。データを取得するプロセス中に、読み込みボックスを表示し、次にデータを取得したときのページ; 別の例として、(ファイル フロー、インターネット データ受信フローなど) の進行状況を表示したい場合がありますStream
。もちろん、StatefulWidget
上記の機能も十分に実現できます。ただし、実際の開発では UI を更新するために非同期データに依存することが非常に一般的であるため、Flutter はこの機能を迅速に実現するための 2 つのコンポーネントFutureBuilder
を特別に提供します。StreamBuilder
フューチャービルダー
FutureBuilder
1 つに依存しFuture
、Future
依存する状態に応じて動的に構築されます。FutureBuilder
コンストラクターを見てみましょう。
FutureBuilder({
this.future,
this.initialData,
required this.builder,
})
-
future
:FutureBuilder
依存性Future
。通常は時間のかかる非同期タスクです。 -
initialData
:初期データ、ユーザーがデフォルトのデータを設定します。 -
builder
:Widget
Builder; このビルダーはFuture
実行のさまざまな段階で複数回呼び出され、ビルダーの署名は次のとおりです。Function (BuildContext context, AsyncSnapshot snapshot)
その中には、
snapshot
現在の非同期タスクのステータス情報と結果情報が含まれます。たとえば、非同期タスクのsnapshot.connectionState
ステータス情報を取得したり、snapshot.hasError
非同期タスクにエラーがあるかどうかを判断したりできます。完全な定義は次のようになります。AsyncSnapshot
クラス定義で表示されます。また、の
FutureBuilder
関数builder
シグネチャはの関数シグネチャと同じですStreamBuilder
。builder
例:ルートを実装します。ルートを開くと、インターネットからデータを取得します。データを取得するとロード ボックスが表示されます。取得が完了すると、成功した場合は取得したデータが表示され、成功した場合は取得したデータが表示されます。失敗するとエラーが表示されます。
ここでは実際にネットワークにアクセスしてデータをリクエストするわけではありませんが、このプロセスをシミュレートして 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
異なるものを返しますwidget
。ConnectionState
は、次のように定義された列挙クラスです。
enum ConnectionState {
none, // 当前没有异步任务,比如[FutureBuilder]的[future]为null时
waiting, // 异步任务处于等待状态
active, // Stream处于激活状态(流上已经有数据传递了),对于FutureBuilder没有该状态。
done, // 异步任务已经终止.
}
ConnectionState.active
にのみ表示されることに注意してくださいStreamBuilder
。
ストリームビルダー
Dart では、Stream
非同期イベント データの受信にも使用されることがわかっています。Future
違いは、複数の非同期操作の結果を受信できることです。ネットワーク コンテンツのダウンロード、ファイルなど、データを複数回読み取る非同期タスクのシナリオでよく使用されます。読み書きなど ストリーム上のイベント(データ)の変化を表示するためのStreamBuilder
UIコンポーネントです。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返回当前时间
});
}
}
操作結果:
次のコードは、StreamBuilder
combin を使用した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
、ダイアログ ボックスの戻り値を受け取るために使用される値を返します: ダイアログ マスクをクリックしてダイアログ ボックスを閉じた場合、値は です。それ以外の場合は、返さFuture
れたnull
値です。例全体を見てみましょう。下: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 ? "中文简体" : "美国英语"}");
}
}
コンポーネントを使用してリスト項目コンポーネントをラップします。これは、ボタンのテキストが左揃えで小さくなることを除いて、SimpleDialogOption
one と同等です。上記の例を実行した後、ユーザーが言語を選択すると、その言語がコンソールに出力されます。TextButton
padding
ダイアログ
実際には両方AlertDialog
ともクラスSimpleDialog
を使用しますDialog
。とAlertDialog
は、サブコンポーネントの実際のサイズを通じて独自のサイズを調整するために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
考えてみましょう。これを次のコードに完全に置き換えることができます。Dialog
Dialog
// 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
新しいダイアログ ルート を直接開きRawDialogRoute
、push
戻り値を返します。ダイアログ ボックスが実際には 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
メソッドが現在のサブツリーに対してのみ再作成されることはわかっていますがcontext
、build
ダイアログ ボックスはメソッド内に構築されず、個別に構築されるため、メソッド内での呼び出しは、構築された UI に影響を与えることはできません。。_DialogRouteState
build
showDialog
_DialogRouteState
context
setState
showDialog
さらに、この現象を別の角度から理解することもできます。前述したように、ダイアログ ボックスもルーティングを通じて実装されているため、上記のコードは実際には親ルート内で子ルートを呼び出して更新しようとしているのと同じです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
実際には継承しているだけでStatelessWidget
、build
メソッド内でカレントを取得したcontext
後に構築メソッドをコールバックに委譲しており、実際には()のコンテキストを取得しているbuilder
ことが分かります。では、同じメソッドを使用してコンテキストを取得し、そのメソッドをプロキシすることはできるでしょうか? 猫の写真を撮り、トラを描いてメソッドをカプセル化しましょう。Builder
StatelessWidget
context
StatefulWidget
build
StatefulBuilder
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; // 更新选中状态
});
},
);
},
),
],
),
実際、このメソッドは本質的に、子コンポーネントが親コンポーネント(StatefulWidget
)build
に、子コンポーネント自体の更新を通知して UI を更新することを意味しており、コードを比較すると理解できます。実はStatefulBuilder
これは Flutter SDK で提供されているクラスであり、Builder
や と同じ原理であり、StatefulBuilder
FlutterBuilder
では非常に実践的なため、しっかり理解する必要があります。
3. 独創的な解決策
もっと簡単な解決策はありますか? この問題を確認するには、まず UI がどのように更新されるかを理解する必要があります。setState
メソッドを呼び出した後にStatefulWidget
再起動されることがわかっていますbuild
。setState
このメソッドは何を行うのでしょうか? そこから抜け出す方法は見つかるでしょうか?この考え方に従って、setState
コアのソース コードを確認する必要があります。
void setState(VoidCallback fn) {
... //省略无关代码
_element.markNeedsBuild();
}
setState
前に述べたElement
ように、Flutter は応答性の高いフレームワークであるため、メソッドが呼び出されたことがわかります。UI をmarkNeedsBuild()
更新するには、状態を変更し、ページをリファクタリングする必要があることをフレームワークに通知するだけです。メソッドは次のとおりElement
ですmarkNeedsBuild()
。この機能を実現します!markNeedsBuild()
メソッドは現在のオブジェクトを " " (ダーティ) としてElement
マークしdirty
、毎回Flutter は " "Frame
とマークされたオブジェクトを再構築します。dirty
Element
その場合、Element
ダイアログ ボックス内の UI オブジェクトを取得し、それを「dirty
」としてマークする方法はありますか? 答えは「はい」です!Context
オブジェクトは によって取得できます。これは実際にはElement
コンポーネント ツリー内のオブジェクトへの参照です。これを理解したら、ソリューションを提供する準備が整いました。次の方法でチェック ボックスを更新可能にできます。context
Element
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 コンポーネントが更新されます。したがって、最善の方法は次のとおりです。 の「スコープ」が狭められ、つまり としてマークのみが付けられ、最適化されたコードは次のようになります。context
rebuild
context
Checkbox
Element
dirty
... //省略无关代码
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
かどうしか選択できません。 「サイズ制限のレイアウト」セクションで述べたように、最初に幅の制約をオフセットしてから、指定された幅を使用することができます。コードは次のとおりです。ConstrainedBox
showDialog
UnconstrainedBox
showDialog
SizedBox
... //省略无关代码
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 の使用を検討できます。コミュニティ。
効果の一部を次に示します。
詳しい使い方は公式ドキュメントを参照してください。
参考: