字节笔记本字节笔记本

flutter 创建底部弹窗

2022-05-24

本文介绍了如何使用Flutter创建一个底部弹窗,并解释了Builder组件的作用以及BuildContext的重要性和使用方法。

import "package:flutter/material.dart";

void main() {
  runApp(const MyApp());
}

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Creating a Modal Bottom Sheet'),
          backgroundColor: Colors.greenAccent,
        ),
        body: Builder(
          builder: (context) {
            return Center(
              child: ElevatedButton(
                child: const Text('Show Modal Bottom Sheet'),
                onPressed: () {
                  showModalBottomSheet(
                    context: context,
                    builder: (context) {
                      return Wrap(
                        children: const [
                          ListTile(
                            leading: Icon(Icons.share),
                            title: Text('Share'),
                          ),
                          ListTile(
                            leading: Icon(Icons.copy),
                            title: Text('Copy Link'),
                          ),
                          ListTile(
                            leading: Icon(Icons.edit),
                            title: Text('Edit'),
                          ),
                        ],
                      );
                    },
                  );
                },
              ),
            );
          },
        ),
      ),
    );
  }
}

一些被遗弃的button组件和替代品

Old Widget Old Theme New Widget New Theme
FlatButton ButtonTheme TextButton TextButtonTheme
RaisedButton ButtonTheme ElevatedButton ElevatedButtonTheme
OutlineButton ButtonTheme OutlinedButton OutlinedButtonTheme

为什么要使用Builder?

Builder 源码

typedef WidgetBuilder = Widget Function(BuildContext context);

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

  final WidgetBuilder builder;

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

build做了一件很简单的事 : 回调一个外部传入builder构建方法,传入context

为什么要传入Context?

MyApp的传入 context 内容如下

context = {StatefulElement} MyApp(dirty, state: _MyAppState#a2d65)
 _parent = {RenderObjectToWidgetElement} [root](renderObject: RenderView#e2db8 NEEDS-LAYOUT NEEDS-PAINT)
 _debugReassembleConfig = null
 _notificationTree = null
 _slot = {Object} 
 _depth = 2
 _widget = {MyApp} MyApp
 _owner = {BuildOwner} 
 _lifecycleState = {_ElementLifecycle} _ElementLifecycle.active
 _debugForgottenChildrenWithGlobalKey = {_HashSet} {}
 _inheritedWidgets = null
 _dependencies = null
 _hadUnsatisfiedDependencies = false
 _dirty = true
 _inDirtyList = false
 _debugBuiltOnce = false
 _debugAllowIgnoredCallsToMarkNeedsBuild = true
 _child = null
 _debugDoingBuild = true
 _state = {_MyAppState} _MyAppState#a2d65
 _didChangeDependencies = false

结构说明如下:

  • Context 的类型为 Element

  • _depth 引用深度为2

  • _child 为 null

  • _parent 当前节点之上只有一个 root 节点

查看BuildContext的源码

abstract class BuildContext {
  /// The current configuration of the [Element] that is this [BuildContext].
  Widget get widget;

  /// The [BuildOwner] for this context. The [BuildOwner] is in charge of
  /// managing the rendering pipeline for this context.
  BuildOwner? get owner;

  /// Whether the [widget] is currently updating the widget or render tree.
  ///
  /// For [StatefulWidget]s and [StatelessWidget]s this flag is true while
  /// their respective build methods are executing.
  /// [RenderObjectWidget]s set this to true while creating or configuring their
  /// associated [RenderObject]s.
  /// Other [Widget] types may set this to true for conceptually similar phases
  /// of their lifecycle.
  ///
  /// When this is true, it is safe for [widget] to establish a dependency to an
  /// [InheritedWidget] by calling [dependOnInheritedElement] or
  /// [dependOnInheritedWidgetOfExactType].
  ///
  /// Accessing this flag in release mode is not valid.
  bool get debugDoingBuild;

  /// The current [RenderObject] for the widget. If the widget is a
  /// [RenderObjectWidget], this is the render object that the widget created
  /// for itself. Otherwise, it is the render object of the first descendant
  /// [RenderObjectWidget].
  ///
  /// This method will only return a valid result after the build phase is
  /// complete. It is therefore not valid to call this from a build method.
  /// It should only be called from interaction event handlers (e.g.
  /// gesture callbacks) or layout or paint callbacks. It is also not valid to
  /// call if [State.mounted] returns false.
  ///
  /// If the render object is a [RenderBox], which is the common case, then the
  /// size of the render object can be obtained from the [size] getter. This is
  /// only valid after the layout phase, and should therefore only be examined
  /// from paint callbacks or interaction event handlers (e.g. gesture
  /// callbacks).
  ///
  /// For details on the different phases of a frame, see the discussion at
  /// [WidgetsBinding.drawFrame].
  ///
  /// Calling this method is theoretically relatively expensive (O(N) in the
  /// depth of the tree), but in practice is usually cheap because the tree
  /// usually has many render objects and therefore the distance to the nearest
  /// render object is usually short.
  RenderObject? findRenderObject();

  /// The size of the [RenderBox] returned by [findRenderObject].
  ///
  /// This getter will only return a valid result after the layout phase is
  /// complete. It is therefore not valid to call this from a build method.
  /// It should only be called from paint callbacks or interaction event
  /// handlers (e.g. gesture callbacks).
  ///
  /// For details on the different phases of a frame, see the discussion at
  /// [WidgetsBinding.drawFrame].
  ///
  /// This getter will only return a valid result if [findRenderObject] actually
  /// returns a [RenderBox]. If [findRenderObject] returns a render object that
  /// is not a subtype of [RenderBox] (e.g., [RenderView]), this getter will
  /// throw an exception in debug mode and will return null in release mode.
  ///
  /// Calling this getter is theoretically relatively expensive (O(N) in the
  /// depth of the tree), but in practice is usually cheap because the tree
  /// usually has many render objects and therefore the distance to the nearest
  /// render object is usually short.
  Size? get size;

  /// Registers this build context with [ancestor] such that when
  /// [ancestor]'s widget changes this build context is rebuilt.
  ///
  /// Returns `ancestor.widget`.
  ///
  /// This method is rarely called directly. Most applications should use
  /// [dependOnInheritedWidgetOfExactType], which calls this method after finding
  /// the appropriate [InheritedElement] ancestor.
  ///
  /// All of the qualifications about when [dependOnInheritedWidgetOfExactType] can
  /// be called apply to this method as well.
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect });

  /// Obtains the nearest widget of the given type `T`, which must be the type of a
  /// concrete [InheritedWidget] subclass, and registers this build context with
  /// that widget such that when that widget changes (or a new widget of that
  /// type is introduced, or the widget goes away), this build context is
  /// rebuilt so that it can obtain new values from that widget.
  ///
  /// This is typically called implicitly from `of()` static methods, e.g.
  /// [Theme.of].
  ///
  /// This method should not be called from widget constructors or from
  /// [State.initState] methods, because those methods would not get called
  /// again if the inherited value were to change. To ensure that the widget
  /// correctly updates itself when the inherited value changes, only call this
  /// (directly or indirectly) from build methods, layout and paint callbacks, or
  /// from [State.didChangeDependencies].
  ///
  /// This method should not be called from [State.dispose] because the element
  /// tree is no longer stable at that time. To refer to an ancestor from that
  /// method, save a reference to the ancestor in [State.didChangeDependencies].
  /// It is safe to use this method from [State.deactivate], which is called
  /// whenever the widget is removed from the tree.
  ///
  /// It is also possible to call this method from interaction event handlers
  /// (e.g. gesture callbacks) or timers, to obtain a value once, if that value
  /// is not going to be cached and reused later.
  ///
  /// Calling this method is O(1) with a small constant factor, but will lead to
  /// the widget being rebuilt more often.
  ///
  /// Once a widget registers a dependency on a particular type by calling this
  /// method, it will be rebuilt, and [State.didChangeDependencies] will be
  /// called, whenever changes occur relating to that widget until the next time
  /// the widget or one of its ancestors is moved (for example, because an
  /// ancestor is added or removed).
  ///
  /// The [aspect] parameter is only used when `T` is an
  /// [InheritedWidget] subclasses that supports partial updates, like
  /// [InheritedModel]. It specifies what "aspect" of the inherited
  /// widget this context depends on.
  T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object? aspect });

  /// Obtains the element corresponding to the nearest widget of the given type `T`,
  /// which must be the type of a concrete [InheritedWidget] subclass.
  ///
  /// Returns null if no such element is found.
  ///
  /// Calling this method is O(1) with a small constant factor.
  ///
  /// This method does not establish a relationship with the target in the way
  /// that [dependOnInheritedWidgetOfExactType] does.
  ///
  /// This method should not be called from [State.dispose] because the element
  /// tree is no longer stable at that time. To refer to an ancestor from that
  /// method, save a reference to the ancestor by calling
  /// [dependOnInheritedWidgetOfExactType] in [State.didChangeDependencies]. It is
  /// safe to use this method from [State.deactivate], which is called whenever
  /// the widget is removed from the tree.
  InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>();

  /// Returns the nearest ancestor widget of the given type `T`, which must be the
  /// type of a concrete [Widget] subclass.
  ///
  /// In general, [dependOnInheritedWidgetOfExactType] is more useful, since
  /// inherited widgets will trigger consumers to rebuild when they change. This
  /// method is appropriate when used in interaction event handlers (e.g.
  /// gesture callbacks) or for performing one-off tasks such as asserting that
  /// you have or don't have a widget of a specific type as an ancestor. The
  /// return value of a Widget's build method should not depend on the value
  /// returned by this method, because the build context will not rebuild if the
  /// return value of this method changes. This could lead to a situation where
  /// data used in the build method changes, but the widget is not rebuilt.
  ///
  /// Calling this method is relatively expensive (O(N) in the depth of the
  /// tree). Only call this method if the distance from this widget to the
  /// desired ancestor is known to be small and bounded.
  ///
  /// This method should not be called from [State.deactivate] or [State.dispose]
  /// because the widget tree is no longer stable at that time. To refer to
  /// an ancestor from one of those methods, save a reference to the ancestor
  /// by calling [findAncestorWidgetOfExactType] in [State.didChangeDependencies].
  ///
  /// Returns null if a widget of the requested type does not appear in the
  /// ancestors of this context.
  T? findAncestorWidgetOfExactType<T extends Widget>();

  /// Returns the [State] object of the nearest ancestor [StatefulWidget] widget
  /// that is an instance of the given type `T`.
  ///
  /// This should not be used from build methods, because the build context will
  /// not be rebuilt if the value that would be returned by this method changes.
  /// In general, [dependOnInheritedWidgetOfExactType] is more appropriate for such
  /// cases. This method is useful for changing the state of an ancestor widget in
  /// a one-off manner, for example, to cause an ancestor scrolling list to
  /// scroll this build context's widget into view, or to move the focus in
  /// response to user interaction.
  ///
  /// In general, though, consider using a callback that triggers a stateful
  /// change in the ancestor rather than using the imperative style implied by
  /// this method. This will usually lead to more maintainable and reusable code
  /// since it decouples widgets from each other.
  ///
  /// Calling this method is relatively expensive (O(N) in the depth of the
  /// tree). Only call this method if the distance from this widget to the
  /// desired ancestor is known to be small and bounded.
  ///
  /// This method should not be called from [State.deactivate] or [State.dispose]
  /// because the widget tree is no longer stable at that time. To refer to
  /// an ancestor from one of those methods, save a reference to the ancestor
  /// by calling [findAncestorStateOfType] in [State.didChangeDependencies].
  ///
  /// {@tool snippet}
  ///
  /// ```dart
  /// ScrollableState? scrollable = context.findAncestorStateOfType<ScrollableState>();
  /// ```
  /// {@end-tool}
  T? findAncestorStateOfType<T extends State>();

  /// Returns the [State] object of the furthest ancestor [StatefulWidget] widget
  /// that is an instance of the given type `T`.
  ///
  /// Functions the same way as [findAncestorStateOfType] but keeps visiting subsequent
  /// ancestors until there are none of the type instance of `T` remaining.
  /// Then returns the last one found.
  ///
  /// This operation is O(N) as well though N is the entire widget tree rather than
  /// a subtree.
  T? findRootAncestorStateOfType<T extends State>();

  /// Returns the [RenderObject] object of the nearest ancestor [RenderObjectWidget] widget
  /// that is an instance of the given type `T`.
  ///
  /// This should not be used from build methods, because the build context will
  /// not be rebuilt if the value that would be returned by this method changes.
  /// In general, [dependOnInheritedWidgetOfExactType] is more appropriate for such
  /// cases. This method is useful only in esoteric cases where a widget needs
  /// to cause an ancestor to change its layout or paint behavior. For example,
  /// it is used by [Material] so that [InkWell] widgets can trigger the ink
  /// splash on the [Material]'s actual render object.
  ///
  /// Calling this method is relatively expensive (O(N) in the depth of the
  /// tree). Only call this method if the distance from this widget to the
  /// desired ancestor is known to be small and bounded.
  ///
  /// This method should not be called from [State.deactivate] or [State.dispose]
  /// because the widget tree is no longer stable at that time. To refer to
  /// an ancestor from one of those methods, save a reference to the ancestor
  /// by calling [findAncestorRenderObjectOfType] in [State.didChangeDependencies].
  T? findAncestorRenderObjectOfType<T extends RenderObject>();

  /// Walks the ancestor chain, starting with the parent of this build context's
  /// widget, invoking the argument for each ancestor. The callback is given a
  /// reference to the ancestor widget's corresponding [Element] object. The
  /// walk stops when it reaches the root widget or when the callback returns
  /// false. The callback must not return null.
  ///
  /// This is useful for inspecting the widget tree.
  ///
  /// Calling this method is relatively expensive (O(N) in the depth of the tree).
  ///
  /// This method should not be called from [State.deactivate] or [State.dispose]
  /// because the element tree is no longer stable at that time. To refer to
  /// an ancestor from one of those methods, save a reference to the ancestor
  /// by calling [visitAncestorElements] in [State.didChangeDependencies].
  void visitAncestorElements(bool Function(Element element) visitor);

  /// Walks the children of this widget.
  ///
  /// This is useful for applying changes to children after they are built
  /// without waiting for the next frame, especially if the children are known,
  /// and especially if there is exactly one child (as is always the case for
  /// [StatefulWidget]s or [StatelessWidget]s).
  ///
  /// Calling this method is very cheap for build contexts that correspond to
  /// [StatefulWidget]s or [StatelessWidget]s (O(1), since there's only one
  /// child).
  ///
  /// Calling this method is potentially expensive for build contexts that
  /// correspond to [RenderObjectWidget]s (O(N) in the number of children).
  ///
  /// Calling this method recursively is extremely expensive (O(N) in the number
  /// of descendants), and should be avoided if possible. Generally it is
  /// significantly cheaper to use an [InheritedWidget] and have the descendants
  /// pull data down, than it is to use [visitChildElements] recursively to push
  /// data down to them.
  void visitChildElements(ElementVisitor visitor);

  /// Start bubbling this notification at the given build context.
  ///
  /// The notification will be delivered to any [NotificationListener] widgets
  /// with the appropriate type parameters that are ancestors of the given
  /// [BuildContext].
  void dispatchNotification(Notification notification);

  /// Returns a description of the [Element] associated with the current build context.
  ///
  /// The `name` is typically something like "The element being rebuilt was".
  ///
  /// See also:
  ///
  ///  * [Element.describeElements], which can be used to describe a list of elements.
  DiagnosticsNode describeElement(String name, {DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty});

  /// Returns a description of the [Widget] associated with the current build context.
  ///
  /// The `name` is typically something like "The widget being rebuilt was".
  DiagnosticsNode describeWidget(String name, {DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty});

  /// Adds a description of a specific type of widget missing from the current
  /// build context's ancestry tree.
  ///
  /// You can find an example of using this method in [debugCheckHasMaterial].
  List<DiagnosticsNode> describeMissingAncestor({ required Type expectedAncestorType });

  /// Adds a description of the ownership chain from a specific [Element]
  /// to the error report.
  ///
  /// The ownership chain is useful for debugging the source of an element.
  DiagnosticsNode describeOwnershipChain(String name);
}
  • BuildContext 是一个抽象类,没有任何具体的实现 ,查看的实现会发现它有且只有一个实现类Element
  • BuildContext作为一个抽象类,暴露对应接口给外部使用,具体的实现由Element完成

No MediaQuery widget ancestor found

  • MediaQuery 是在 MaterialApp 内部包含的

  • 如果在MaterialApp并行的节点使用MediaQuery.of (context)找不到

     static MediaQueryData of(BuildContext context) {
        assert(context != null);
        assert(debugCheckHasMediaQuery(context));
        return context.dependOnInheritedWidgetOfExactType<MediaQuery>()!.data;
      }
    

    MediaQuery.of : 它接受一个BuildContext类型的参数,并通过调用context.dependOnInheritedWidgetOfExactType方法来获得一个最近的widget的实例,其类型为MediaQuery

    context.dependOnInheritedWidgetOfExactType()只会找到最近节点

    查看 dependOnInheritedWidgetOfExactType 源码,如下

     @override
      T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({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;
      }
    

    再进一步到,dependOnInheritedElement 源码

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

    返回ancestor.widget,也就是 _inheritedWidgets

  • 每一个element mounted的时候会调用一个_updateInheritance方法

    @mustCallSuper
      void mount(Element parent, dynamic newSlot) {
        //... 
        _updateInheritance();
        //...
      }
    

    _updateInheritance的实现是这样的

     void _updateInheritance() {
        assert(_active);
        _inheritedWidgets = _parent?._inheritedWidgets;
      }
    

    _parent?._inheritedWidgets:每一个子类中都保存一份副本,该副本始终保存了所有父节点中存在的InheritedElement。

    在InheritedElement中的实现是

      @override
      void _updateInheritance() {
        assert(_active);
        final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
        if (incomingWidgets != null)
          _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
        else
          _inheritedWidgets = HashMap<Type, InheritedElement>();
        _inheritedWidgets[widget.runtimeType] = this;
      }
    

    这个除了继承父节点的_inheritedWidgets外,还把自身也加进map了,到此整个链路就完成了,所以该map里面始终保存着父节点所有的InheritedElement,但是由于是map保存的,如果是父节点有两个或更多的同一类型的InheritedElement,那么Map中只会保存最近的一个,其余的会被覆盖掉,这也是为什么我们使用context.dependOnInheritedWidgetOfExactType()只会找到最近节点的原因