inherited_theme.dart 8.04 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'framework.dart';

/// An [InheritedWidget] that defines visual properties like colors
/// and text styles, which the [child]'s subtree depends on.
///
10 11 12
/// The [wrap] method is used by [captureAll] and [CapturedThemes.wrap] to
/// construct a widget that will wrap a child in all of the inherited themes
/// which are present in a specified part of the widget tree.
13
///
14 15 16
/// A widget that's shown in a different context from the one it's built in,
/// like the contents of a new route or an overlay, will be able to see the
/// ancestor inherited themes of the context it was built in.
17
///
18
/// {@tool dartpad --template=freeform}
19
/// This example demonstrates how `InheritedTheme.capture()` can be used
20
/// to wrap the contents of a new route with the inherited themes that
21
/// are present when the route was built - but are not present when route
22 23
/// is actually shown.
///
24
/// If the same code is run without `InheritedTheme.capture(), the
25 26 27 28 29 30 31 32 33
/// new route's Text widget will inherit the "something must be wrong"
/// fallback text style, rather than the default text style defined in MyApp.
///
/// ```dart imports
/// import 'package:flutter/material.dart';
/// ```
///
/// ```dart main
/// void main() {
34
///   runApp(const MyApp());
35 36 37 38 39
/// }
/// ```
///
/// ```dart
/// class MyAppBody extends StatelessWidget {
40 41
///   const MyAppBody({Key? key}) : super(key: key);
///
42 43
///   @override
///   Widget build(BuildContext context) {
44 45 46
///     final NavigatorState navigator = Navigator.of(context);
///     // This InheritedTheme.capture() saves references to themes that are
///     // found above the context provided to this widget's build method
nt4f04uNd's avatar
nt4f04uNd committed
47
///     // excluding themes are found above the navigator. Those themes do
48 49 50 51 52
///     // not have to be captured, because they will already be visible from
///     // the new route pushed onto said navigator.
///     // Themes are captured outside of the route's builder because when the
///     // builder executes, the context may not be valid anymore.
///     final CapturedThemes themes = InheritedTheme.capture(from: context, to: navigator.context);
53 54 55
///     return GestureDetector(
///       onTap: () {
///         Navigator.of(context).push(
56
///           MaterialPageRoute<void>(
57
///             builder: (BuildContext _) {
58 59
///               // Wrap the actual child of the route in the previously
///               // captured themes.
60 61 62 63 64 65 66
///               return themes.wrap(
///                 Container(
///                   alignment: Alignment.center,
///                   color: Colors.white,
///                   child: const Text('Hello World'),
///                 ),
///               );
67 68 69 70
///             },
///           ),
///         );
///       },
71
///       child: const Center(child: Text('Tap Here')),
72 73 74 75 76
///     );
///   }
/// }
///
/// class MyApp extends StatelessWidget {
77 78
///   const MyApp({Key? key}) : super(key: key);
///
79 80
///   @override
///   Widget build(BuildContext context) {
81
///     return const MaterialApp(
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
///       home: Scaffold(
///         // Override the DefaultTextStyle defined by the Scaffold.
///         // Descendant widgets will inherit this big blue text style.
///         body: DefaultTextStyle(
///           style: TextStyle(fontSize: 48, color: Colors.blue),
///           child: MyAppBody(),
///         ),
///       ),
///     );
///   }
/// }
/// ```
/// {@end-tool}
abstract class InheritedTheme extends InheritedWidget {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.

  const InheritedTheme({
100 101
    Key? key,
    required Widget child,
102 103 104 105 106
  }) : super(key: key, child: child);

  /// Return a copy of this inherited theme with the specified [child].
  ///
  /// This implementation for [TooltipTheme] is typical:
107
  ///
108 109
  /// ```dart
  /// Widget wrap(BuildContext context, Widget child) {
110
  ///   return TooltipTheme(data: data, child: child);
111 112 113 114
  /// }
  /// ```
  Widget wrap(BuildContext context, Widget child);

115 116 117 118 119 120 121 122 123 124 125 126 127 128
  /// Returns a widget that will [wrap] `child` in all of the inherited themes
  /// which are present between `context` and the specified `to`
  /// [BuildContext].
  ///
  /// The `to` context must be an ancestor of `context`. If `to` is not
  /// specified, all inherited themes up to the root of the widget tree are
  /// captured.
  ///
  /// After calling this method, the themes present between `context` and `to`
  /// are frozen for the provided `child`. If the themes (or their theme data)
  /// change in the original subtree, those changes will not be visible to
  /// the wrapped `child` - unless this method is called again to re-wrap the
  /// child.
  static Widget captureAll(BuildContext context, Widget child, {BuildContext? to}) {
129 130 131
    assert(child != null);
    assert(context != null);

132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
    return capture(from: context, to: to).wrap(child);
  }

  /// Returns a [CapturedThemes] object that includes all the [InheritedTheme]s
  /// between the given `from` and `to` [BuildContext]s.
  ///
  /// The `to` context must be an ancestor of the `from` context. If `to` is
  /// null, all ancestor inherited themes of `from` up to the root of the
  /// widget tree are captured.
  ///
  /// After calling this method, the themes present between `from` and `to` are
  /// frozen in the returned [CapturedThemes] object. If the themes (or their
  /// theme data) change in the original subtree, those changes will not be
  /// applied to the themes captured in the [CapturedThemes] object - unless
  /// this method is called again to re-capture the updated themes.
  ///
  /// To wrap a [Widget] in the captured themes, call [CapturedThemes.wrap].
149 150 151
  ///
  /// This method can be expensive if there are many widgets between `from` and
  /// `to` (it walks the element tree between those nodes).
152 153 154 155 156 157 158 159
  static CapturedThemes capture({ required BuildContext from, required BuildContext? to }) {
    assert(from != null);

    if (from == to) {
      // Nothing to capture.
      return CapturedThemes._(const <InheritedTheme>[]);
    }

160
    final List<InheritedTheme> themes = <InheritedTheme>[];
161
    final Set<Type> themeTypes = <Type>{};
162 163 164 165 166 167 168 169 170 171 172 173 174
    late bool debugDidFindAncestor;
    assert(() {
      debugDidFindAncestor = to == null;
      return true;
    }());
    from.visitAncestorElements((Element ancestor) {
      if (ancestor == to) {
        assert(() {
          debugDidFindAncestor = true;
          return true;
        }());
        return false;
      }
175
      if (ancestor is InheritedElement && ancestor.widget is InheritedTheme) {
176
        final InheritedTheme theme = ancestor.widget as InheritedTheme;
177 178 179
        final Type themeType = theme.runtimeType;
        // Only remember the first theme of any type. This assumes
        // that inherited themes completely shadow ancestors of the
Pierre-Louis's avatar
Pierre-Louis committed
180
        // same type.
181 182 183 184
        if (!themeTypes.contains(themeType)) {
          themeTypes.add(themeType);
          themes.add(theme);
        }
185 186 187 188
      }
      return true;
    });

189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
    assert(debugDidFindAncestor, 'The provided `to` context must be an ancestor of the `from` context.');
    return CapturedThemes._(themes);
  }
}

/// Stores a list of captured [InheritedTheme]s that can be wrapped around a
/// child [Widget].
///
/// Used as return type by [InheritedTheme.capture].
class CapturedThemes {
  CapturedThemes._(this._themes);

  final List<InheritedTheme> _themes;

  /// Wraps a `child` [Widget] in the [InheritedTheme]s captured in this object.
  Widget wrap(Widget child) {
    return _CaptureAll(themes: _themes, child: child);
206 207 208 209 210
  }
}

class _CaptureAll extends StatelessWidget {
  const _CaptureAll({
211 212 213
    Key? key,
    required this.themes,
    required this.child,
214 215 216 217 218 219 220 221
  }) : assert(themes != null), assert(child != null), super(key: key);

  final List<InheritedTheme> themes;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    Widget wrappedChild = child;
222
    for (final InheritedTheme theme in themes)
223 224 225 226
      wrappedChild = theme.wrap(context, wrappedChild);
    return wrappedChild;
  }
}