Commit 6399a3af authored by Adam Barth's avatar Adam Barth Committed by GitHub

Fix DropdownButton regression (#6353)

When I changed how routes complete their futures, I broke the Dropdown
button because it was still waiting for its own Completer to complete
instead of using the Future returned by push. This patch fixes that
issue.

I've also removed the previous behavior of the DropdownButton forwarding
its text style to its route. The mechansim that we were using doesn't
work properly in all cases. For example, if the DropdownButton is a
child of a LayoutBuilder, then the route will have already built by the
time the DropdownButton gets a chance to forward its text style, causing
an assert in setState.

Finally, I've tweaked PopupMenuButton to work the same way as
DropdownButton in a couple corner cases (e.g., not calling the changed
callback if the button was removed from the tree before the menu
completed its Future).

Fixes #6352
parent e81e06a6
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
...@@ -293,8 +292,8 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> { ...@@ -293,8 +292,8 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
this.selectedIndex, this.selectedIndex,
this.elevation: 8, this.elevation: 8,
this.theme, this.theme,
TextStyle style, this.style,
}) : _style = style { }) {
assert(style != null); assert(style != null);
} }
...@@ -304,21 +303,12 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> { ...@@ -304,21 +303,12 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
final int selectedIndex; final int selectedIndex;
final int elevation; final int elevation;
final ThemeData theme; final ThemeData theme;
final TextStyle style;
// The layout gets this route's scrollableKey so that it can scroll the // The layout gets this route's scrollableKey so that it can scroll the
/// selected item into position, but only on the initial layout. /// selected item into position, but only on the initial layout.
bool initialLayout = true; bool initialLayout = true;
TextStyle get style => _style;
TextStyle _style;
set style (TextStyle value) {
assert(value != null);
if (_style == value)
return;
setState(() {
_style = value;
});
}
@override @override
Duration get transitionDuration => _kDropdownMenuDuration; Duration get transitionDuration => _kDropdownMenuDuration;
...@@ -493,24 +483,17 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> { ...@@ -493,24 +483,17 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> {
TextStyle get _textStyle => config.style ?? Theme.of(context).textTheme.subhead; TextStyle get _textStyle => config.style ?? Theme.of(context).textTheme.subhead;
_DropdownRoute<T> _currentRoute;
void _handleTap() { void _handleTap() {
assert(_currentRoute == null);
final RenderBox itemBox = context.findRenderObject(); final RenderBox itemBox = context.findRenderObject();
final Rect itemRect = itemBox.localToGlobal(Point.origin) & itemBox.size; final Rect itemRect = itemBox.localToGlobal(Point.origin) & itemBox.size;
final Completer<_DropdownRouteResult<T>> completer = new Completer<_DropdownRouteResult<T>>(); Navigator.push(context, new _DropdownRoute<T>(
_currentRoute = new _DropdownRoute<T>(
items: config.items, items: config.items,
buttonRect: _kMenuHorizontalPadding.inflateRect(itemRect), buttonRect: _kMenuHorizontalPadding.inflateRect(itemRect),
selectedIndex: _selectedIndex, selectedIndex: _selectedIndex,
elevation: config.elevation, elevation: config.elevation,
theme: Theme.of(context, shadowThemeOnly: true), theme: Theme.of(context, shadowThemeOnly: true),
style: _textStyle, style: _textStyle,
); )).then((_DropdownRouteResult<T> newValue) {
Navigator.push(context, _currentRoute);
completer.future.then((_DropdownRouteResult<T> newValue) {
_currentRoute = null;
if (!mounted || newValue == null) if (!mounted || newValue == null)
return; return;
if (config.onChanged != null) if (config.onChanged != null)
...@@ -521,10 +504,8 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> { ...@@ -521,10 +504,8 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
final TextStyle style = _textStyle;
_currentRoute?.style = style;
Widget result = new DefaultTextStyle( Widget result = new DefaultTextStyle(
style: style, style: _textStyle,
child: new Row( child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
......
...@@ -508,7 +508,7 @@ class PopupMenuButton<T> extends StatefulWidget { ...@@ -508,7 +508,7 @@ class PopupMenuButton<T> extends StatefulWidget {
} }
class _PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { class _PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
void showButtonMenu(BuildContext context) { void showButtonMenu() {
final RenderBox renderBox = context.findRenderObject(); final RenderBox renderBox = context.findRenderObject();
final Point topLeft = renderBox.localToGlobal(Point.origin); final Point topLeft = renderBox.localToGlobal(Point.origin);
showMenu/*<T>*/( showMenu/*<T>*/(
...@@ -521,9 +521,11 @@ class _PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { ...@@ -521,9 +521,11 @@ class _PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
0.0, 0.0 0.0, 0.0
) )
) )
.then((T value) { .then((T newValue) {
if (value != null && config.onSelected != null) if (!mounted || newValue == null)
config.onSelected(value); return;
if (config.onSelected != null)
config.onSelected(newValue);
}); });
} }
...@@ -534,12 +536,12 @@ class _PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { ...@@ -534,12 +536,12 @@ class _PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
icon: new Icon(Icons.more_vert), icon: new Icon(Icons.more_vert),
padding: config.padding, padding: config.padding,
tooltip: config.tooltip, tooltip: config.tooltip,
onPressed: () { showButtonMenu(context); } onPressed: showButtonMenu,
); );
} }
return new InkWell( return new InkWell(
onTap: () { showButtonMenu(context); }, onTap: showButtonMenu,
child: config.child child: config.child,
); );
} }
} }
...@@ -6,6 +6,64 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -6,6 +6,64 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
void main() { void main() {
testWidgets('Drop down button control test', (WidgetTester tester) async {
List<String> items = <String>['one', 'two', 'three', 'four'];
String value = items.first;
void didChangeValue(String newValue) {
value = newValue;
}
Widget build() {
return new MaterialApp(
home: new Material(
child: new Center(
child: new DropdownButton<String>(
value: value,
items: items.map((String item) {
return new DropdownMenuItem<String>(
value: item,
child: new Text(item),
);
}).toList(),
onChanged: didChangeValue,
),
),
),
);
}
await tester.pumpWidget(build());
await tester.tap(find.text('one'));
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
expect(value, equals('one'));
await tester.tap(find.text('three').last);
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
expect(value, equals('three'));
await tester.tap(find.text('three'));
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
expect(value, equals('three'));
await tester.pumpWidget(build());
await tester.tap(find.text('two').last);
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
expect(value, equals('two'));
});
testWidgets('Drop down screen edges', (WidgetTester tester) async { testWidgets('Drop down screen edges', (WidgetTester tester) async {
int value = 4; int value = 4;
List<DropdownMenuItem<int>> items = <DropdownMenuItem<int>>[]; List<DropdownMenuItem<int>> items = <DropdownMenuItem<int>>[];
...@@ -19,7 +77,7 @@ void main() { ...@@ -19,7 +77,7 @@ void main() {
DropdownButton<int> button = new DropdownButton<int>( DropdownButton<int> button = new DropdownButton<int>(
value: value, value: value,
onChanged: handleChanged, onChanged: handleChanged,
items: items items: items,
); );
await tester.pumpWidget( await tester.pumpWidget(
...@@ -27,10 +85,10 @@ void main() { ...@@ -27,10 +85,10 @@ void main() {
home: new Material( home: new Material(
child: new Align( child: new Align(
alignment: FractionalOffset.topCenter, alignment: FractionalOffset.topCenter,
child: button child: button,
) ),
) ),
) ),
); );
await tester.tap(find.text('4')); await tester.tap(find.text('4'));
...@@ -48,13 +106,13 @@ void main() { ...@@ -48,13 +106,13 @@ void main() {
expect(value, 4); expect(value, 4);
await tester.tap(find.byConfig(button)); await tester.tap(find.byConfig(button));
expect(value, 4); expect(value, 4);
await tester.idle(); // this waits for the route's completer to complete, which calls handleChanged // this waits for the route's completer to complete, which calls handleChanged
await tester.idle();
expect(value, 4); expect(value, 4);
// TODO(abarth): Remove these calls to pump once navigator cleans up its // TODO(abarth): Remove these calls to pump once navigator cleans up its
// pop transitions. // pop transitions.
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the menu animation await tester.pump(const Duration(seconds: 1)); // finish the menu animation
}); });
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment