Merge pull request #812 from abarth/check_material

Widgets that depend on Material should assert that
diff --git a/packages/flutter/lib/src/material/checkbox.dart b/packages/flutter/lib/src/material/checkbox.dart
index 0cb5a56..55d8a8f 100644
--- a/packages/flutter/lib/src/material/checkbox.dart
+++ b/packages/flutter/lib/src/material/checkbox.dart
@@ -8,6 +8,7 @@
 import 'package:flutter/widgets.dart';
 
 import 'constants.dart';
+import 'debug.dart';
 import 'theme.dart';
 import 'toggleable.dart';
 
@@ -35,6 +36,7 @@
   final ValueChanged<bool> onChanged;
 
   Widget build(BuildContext context) {
+    assert(debugCheckHasMaterial(context));
     ThemeData themeData = Theme.of(context);
     return new _CheckboxRenderObjectWidget(
       value: value,
diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart
index 73aa1b7..33ebb7d 100644
--- a/packages/flutter/lib/src/material/chip.dart
+++ b/packages/flutter/lib/src/material/chip.dart
@@ -5,6 +5,7 @@
 import 'package:flutter/widgets.dart';
 
 import 'colors.dart';
+import 'debug.dart';
 import 'icon.dart';
 
 const double _kChipHeight = 32.0;
@@ -34,6 +35,7 @@
   final VoidCallback onDeleted;
 
   Widget build(BuildContext context) {
+    assert(debugCheckHasMaterial(context));
     final bool deletable = onDeleted != null;
     double leftPadding = 12.0;
     double rightPadding = 12.0;
diff --git a/packages/flutter/lib/src/material/debug.dart b/packages/flutter/lib/src/material/debug.dart
new file mode 100644
index 0000000..086a566
--- /dev/null
+++ b/packages/flutter/lib/src/material/debug.dart
@@ -0,0 +1,18 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/widgets.dart';
+
+import 'material.dart';
+
+bool debugCheckHasMaterial(BuildContext context) {
+  assert(() {
+    if (context.widget is Material || context.ancestorWidgetOfType(Material) != null)
+      return true;
+    Element element = context;
+    debugPrint('${context.widget} needs to be placed inside a Material widget. Ownership chain:\n${element.debugGetOwnershipChain(10)}');
+    return false;
+  });
+  return true;
+}
diff --git a/packages/flutter/lib/src/material/drawer_header.dart b/packages/flutter/lib/src/material/drawer_header.dart
index 650d0f2..a004bf2 100644
--- a/packages/flutter/lib/src/material/drawer_header.dart
+++ b/packages/flutter/lib/src/material/drawer_header.dart
@@ -5,6 +5,7 @@
 import 'package:flutter/widgets.dart';
 
 import 'constants.dart';
+import 'debug.dart';
 import 'theme.dart';
 
 // TODO(jackson): This class should usually render the user's
@@ -16,6 +17,7 @@
   final Widget child;
 
   Widget build(BuildContext context) {
+    assert(debugCheckHasMaterial(context));
     return new Container(
       height: kStatusBarHeight + kMaterialDrawerHeight,
       decoration: new BoxDecoration(
diff --git a/packages/flutter/lib/src/material/dropdown.dart b/packages/flutter/lib/src/material/dropdown.dart
index c094cf7..b4984e0 100644
--- a/packages/flutter/lib/src/material/dropdown.dart
+++ b/packages/flutter/lib/src/material/dropdown.dart
@@ -9,6 +9,7 @@
 import 'package:flutter/rendering.dart';
 import 'package:flutter/widgets.dart';
 
+import 'debug.dart';
 import 'icon.dart';
 import 'ink_well.dart';
 import 'shadows.dart';
@@ -262,6 +263,7 @@
   }
 
   Widget build(BuildContext context) {
+    assert(debugCheckHasMaterial(context));
     return new GestureDetector(
       onTap: _handleTap,
       child: new Container(
diff --git a/packages/flutter/lib/src/material/ink_well.dart b/packages/flutter/lib/src/material/ink_well.dart
index 9e3419f..a70d53f 100644
--- a/packages/flutter/lib/src/material/ink_well.dart
+++ b/packages/flutter/lib/src/material/ink_well.dart
@@ -6,6 +6,7 @@
 import 'package:flutter/rendering.dart';
 import 'package:flutter/widgets.dart';
 
+import 'debug.dart';
 import 'material.dart';
 import 'theme.dart';
 
@@ -137,6 +138,7 @@
   }
 
   Widget build(BuildContext context) {
+    assert(debugCheckHasMaterial(context));
     final bool enabled = config.onTap != null || config.onDoubleTap != null || config.onLongPress != null;
     return new GestureDetector(
       onTapDown: enabled ? _handleTapDown : null,
diff --git a/packages/flutter/lib/src/material/input.dart b/packages/flutter/lib/src/material/input.dart
index 5b904f9..f8f2aa7 100644
--- a/packages/flutter/lib/src/material/input.dart
+++ b/packages/flutter/lib/src/material/input.dart
@@ -7,6 +7,7 @@
 import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart';
 
+import 'debug.dart';
 import 'theme.dart';
 
 export 'package:flutter/rendering.dart' show ValueChanged;
@@ -75,6 +76,7 @@
   }
 
   Widget buildContent(BuildContext context) {
+    assert(debugCheckHasMaterial(context));
     ThemeData themeData = Theme.of(context);
     bool focused = Focus.at(context, config);
 
diff --git a/packages/flutter/lib/src/material/material_button.dart b/packages/flutter/lib/src/material/material_button.dart
index 9503752..cfc825c 100644
--- a/packages/flutter/lib/src/material/material_button.dart
+++ b/packages/flutter/lib/src/material/material_button.dart
@@ -5,6 +5,7 @@
 import 'package:flutter/widgets.dart';
 
 import 'colors.dart';
+import 'debug.dart';
 import 'ink_well.dart';
 import 'material.dart';
 import 'theme.dart';
@@ -98,6 +99,7 @@
   }
 
   Widget build(BuildContext context) {
+    assert(debugCheckHasMaterial(context));
     Widget contents = new InkWell(
       onTap: config.onPressed,
       onHighlightChanged: _handleHighlightChanged,
diff --git a/packages/flutter/lib/src/material/radio.dart b/packages/flutter/lib/src/material/radio.dart
index b9866a9..1983dd7 100644
--- a/packages/flutter/lib/src/material/radio.dart
+++ b/packages/flutter/lib/src/material/radio.dart
@@ -8,6 +8,7 @@
 import 'package:flutter/widgets.dart';
 
 import 'constants.dart';
+import 'debug.dart';
 import 'theme.dart';
 import 'toggleable.dart';
 
@@ -39,6 +40,7 @@
   }
 
   Widget build(BuildContext context) {
+    assert(debugCheckHasMaterial(context));
     ThemeData themeData = Theme.of(context);
     return new _RadioRenderObjectWidget(
       selected: value == groupValue,
diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart
index 08c4e36..a94e26d 100644
--- a/packages/flutter/lib/src/material/slider.dart
+++ b/packages/flutter/lib/src/material/slider.dart
@@ -10,6 +10,7 @@
 
 import 'colors.dart';
 import 'constants.dart';
+import 'debug.dart';
 import 'theme.dart';
 
 class Slider extends StatelessComponent {
@@ -37,6 +38,7 @@
   }
 
   Widget build(BuildContext context) {
+    assert(debugCheckHasMaterial(context));
     return new _SliderRenderObjectWidget(
       value: (value - min) / (max - min),
       primaryColor: Theme.of(context).accentColor,
diff --git a/packages/flutter/lib/src/material/switch.dart b/packages/flutter/lib/src/material/switch.dart
index 8ced196..dc2577f 100644
--- a/packages/flutter/lib/src/material/switch.dart
+++ b/packages/flutter/lib/src/material/switch.dart
@@ -11,6 +11,7 @@
 
 import 'colors.dart';
 import 'constants.dart';
+import 'debug.dart';
 import 'shadows.dart';
 import 'theme.dart';
 import 'toggleable.dart';
@@ -23,6 +24,7 @@
   final ValueChanged<bool> onChanged;
 
   Widget build(BuildContext context) {
+    assert(debugCheckHasMaterial(context));
     ThemeData themeData = Theme.of(context);
     final isDark = themeData.brightness == ThemeBrightness.dark;
 
diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart
index 71e67b1..48c4c7a 100644
--- a/packages/flutter/lib/src/widgets/binding.dart
+++ b/packages/flutter/lib/src/widgets/binding.dart
@@ -76,6 +76,7 @@
   void _runApp(Widget app) {
     _renderViewElement = new RenderObjectToWidgetAdapter<RenderBox>(
       container: renderView,
+      debugShortDescription: '[root]',
       child: app
     ).attachToRenderTree(_renderViewElement);
     beginFrame();
@@ -102,11 +103,15 @@
 /// RenderObjectWithChildMixin protocol. The type argument T is the kind of
 /// RenderObject that the container expects as its child.
 class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {
-  RenderObjectToWidgetAdapter({ this.child, RenderObjectWithChildMixin<T> container })
-    : container = container, super(key: new GlobalObjectKey(container));
+  RenderObjectToWidgetAdapter({
+    this.child,
+    RenderObjectWithChildMixin<T> container,
+    this.debugShortDescription
+  }) : container = container, super(key: new GlobalObjectKey(container));
 
   final Widget child;
   final RenderObjectWithChildMixin<T> container;
+  final String debugShortDescription;
 
   RenderObjectToWidgetElement<T> createElement() => new RenderObjectToWidgetElement<T>(this);
 
@@ -125,6 +130,8 @@
     }, building: true);
     return element;
   }
+
+  String toStringShort() => debugShortDescription ?? super.toStringShort();
 }
 
 /// This element class is the instantiation of a [RenderObjectToWidgetAdapter].
diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart
index 69f7677..81eeb12 100644
--- a/packages/flutter/lib/src/widgets/framework.dart
+++ b/packages/flutter/lib/src/widgets/framework.dart
@@ -875,6 +875,18 @@
     assert(false);
   }
 
+  String debugGetOwnershipChain(int limit) {
+    List<String> chain = <String>[];
+    Element node = this;
+    while (chain.length < limit && node != null) {
+      chain.add(node.toStringShort());
+      node = node._parent;
+    }
+    if (node != null)
+      chain.add('\u22EF');
+    return chain.join(' \u2190 ');
+  }
+
   String toStringShort() {
     return widget != null ? '${widget.toStringShort()}' : '[$runtimeType]';
   }
@@ -1354,15 +1366,7 @@
   }
 
   void debugUpdateRenderObjectOwner() {
-    List<String> chain = <String>[];
-    Element node = this;
-    while (chain.length < 4 && node != null) {
-      chain.add(node.toStringShort());
-      node = node._parent;
-    }
-    if (node != null)
-      chain.add('\u22EF');
-    _renderObject.debugOwner = chain.join(' \u2190 ');
+    _renderObject.debugOwner = debugGetOwnershipChain(4);
   }
 
   void performRebuild() {
diff --git a/packages/unit/test/widget/input_test.dart b/packages/unit/test/widget/input_test.dart
index 3dedf70..2e9c21b 100644
--- a/packages/unit/test/widget/input_test.dart
+++ b/packages/unit/test/widget/input_test.dart
@@ -38,10 +38,12 @@
 
       Widget builder() {
         return new Center(
-          child: new Input(
-            key: inputKey,
-            placeholder: 'Placeholder',
-            onChanged: (String value) { inputValue = value; }
+          child: new Material(
+            child: new Input(
+              key: inputKey,
+              placeholder: 'Placeholder',
+              onChanged: (String value) { inputValue = value; }
+            )
           )
         );
       }
@@ -72,9 +74,11 @@
 
       Widget builder() {
         return new Center(
-          child: new Input(
-            key: inputKey,
-            placeholder: 'Placeholder'
+          child: new Material(
+            child: new Input(
+              key: inputKey,
+              placeholder: 'Placeholder'
+            )
           )
         );
       }
@@ -112,9 +116,11 @@
 
       Widget builder() {
         return new Center(
-          child: new Input(
-            key: inputKey,
-            placeholder: 'Placeholder'
+          child: new Material(
+            child: new Input(
+              key: inputKey,
+              placeholder: 'Placeholder'
+            )
           )
         );
       }
@@ -145,10 +151,12 @@
 
       Widget builder() {
         return new Center(
-          child: new Input(
-            key: inputKey,
-            hideText: true,
-            placeholder: 'Placeholder'
+          child: new Material(
+            child: new Input(
+              key: inputKey,
+              hideText: true,
+              placeholder: 'Placeholder'
+            )
           )
         );
       }