From 0c871b8528a3e28ee088affe7c19b0628b913423 Mon Sep 17 00:00:00 2001 From: Shi-Hao Hong Date: Wed, 1 May 2019 12:52:52 -0700 Subject: [PATCH] Improve RadioListTile Callback Behavior Consistency (#31574) --- .../lib/src/material/radio_list_tile.dart | 2 +- .../test/material/control_list_tile_test.dart | 179 ++++++++++++++++-- 2 files changed, 168 insertions(+), 13 deletions(-) diff --git a/packages/flutter/lib/src/material/radio_list_tile.dart b/packages/flutter/lib/src/material/radio_list_tile.dart index fae574cac6a..bb428f47b45 100644 --- a/packages/flutter/lib/src/material/radio_list_tile.dart +++ b/packages/flutter/lib/src/material/radio_list_tile.dart @@ -231,7 +231,7 @@ class RadioListTile extends StatelessWidget { isThreeLine: isThreeLine, dense: dense, enabled: onChanged != null, - onTap: onChanged != null ? () { onChanged(value); } : null, + onTap: onChanged != null && !checked ? () { onChanged(value); } : null, selected: selected, ), ), diff --git a/packages/flutter/test/material/control_list_tile_test.dart b/packages/flutter/test/material/control_list_tile_test.dart index 9351ba801b3..a2939fe2624 100644 --- a/packages/flutter/test/material/control_list_tile_test.dart +++ b/packages/flutter/test/material/control_list_tile_test.dart @@ -34,20 +34,175 @@ void main() { expect(log, equals([false, '-', false])); }); - testWidgets('RadioListTile control test', (WidgetTester tester) async { + testWidgets('RadioListTile should initialize according to groupValue', (WidgetTester tester) async { + final List values = [0, 1, 2]; + int selectedValue; + // Constructor parameters are required for [RadioListTile], but they are + // irrelevant when searching with [find.byType]. + final Type radioListTileType = const RadioListTile( + value: 0, + groupValue: 0, + onChanged: null, + ).runtimeType; + + List> generatedRadioListTiles; + List> findTiles() => find + .byType(radioListTileType) + .evaluate() + .map>((Element element) => element.widget) + .toList(); + + Widget buildFrame() { + return wrap( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Scaffold( + body: ListView.builder( + itemCount: values.length, + itemBuilder: (BuildContext context, int index) => RadioListTile( + onChanged: (int value) { + setState(() { selectedValue = value; }); + }, + value: values[index], + groupValue: selectedValue, + title: Text(values[index].toString()), + ), + ), + ); + }, + ), + ); + } + + await tester.pumpWidget(buildFrame()); + generatedRadioListTiles = findTiles(); + + expect(generatedRadioListTiles[0].checked, equals(false)); + expect(generatedRadioListTiles[1].checked, equals(false)); + expect(generatedRadioListTiles[2].checked, equals(false)); + + selectedValue = 1; + + await tester.pumpWidget(buildFrame()); + generatedRadioListTiles = findTiles(); + + expect(generatedRadioListTiles[0].checked, equals(false)); + expect(generatedRadioListTiles[1].checked, equals(true)); + expect(generatedRadioListTiles[2].checked, equals(false)); + }); + + testWidgets('RadioListTile control tests', (WidgetTester tester) async { + final List values = [0, 1, 2]; + int selectedValue; + // Constructor parameters are required for [Radio], but they are irrelevant + // when searching with [find.byType]. + final Type radioType = const Radio( + value: 0, + groupValue: 0, + onChanged: null, + ).runtimeType; final List log = []; - await tester.pumpWidget(wrap( - child: RadioListTile( - value: true, - groupValue: false, - onChanged: (bool value) { log.add(value); }, - title: const Text('Hello'), - ), - )); - await tester.tap(find.text('Hello')); + + Widget buildFrame() { + return wrap( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Scaffold( + body: ListView.builder( + itemCount: values.length, + itemBuilder: (BuildContext context, int index) => RadioListTile( + onChanged: (int value) { + log.add(value); + setState(() { selectedValue = value; }); + }, + value: values[index], + groupValue: selectedValue, + title: Text(values[index].toString()), + ), + ), + ); + }, + ), + ); + } + + // Tests for tapping between [Radio] and [ListTile] + await tester.pumpWidget(buildFrame()); + await tester.tap(find.text('1')); log.add('-'); - await tester.tap(find.byType(const Radio(value: false, groupValue: false, onChanged: null).runtimeType)); - expect(log, equals([true, '-', true])); + await tester.tap(find.byType(radioType).at(2)); + expect(log, equals([1, '-', 2])); + log.add('-'); + await tester.tap(find.text('1')); + + log.clear(); + selectedValue = null; + + // Tests for tapping across [Radio]s exclusively + await tester.pumpWidget(buildFrame()); + await tester.tap(find.byType(radioType).at(1)); + log.add('-'); + await tester.tap(find.byType(radioType).at(2)); + expect(log, equals([1, '-', 2])); + + log.clear(); + selectedValue = null; + + // Tests for tapping across [ListTile]s exclusively + await tester.pumpWidget(buildFrame()); + await tester.tap(find.text('1')); + log.add('-'); + await tester.tap(find.text('2')); + expect(log, equals([1, '-', 2])); + }); + + testWidgets('Selected RadioListTile should not trigger onChanged', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/30311 + final List values = [0, 1, 2]; + int selectedValue; + // Constructor parameters are required for [Radio], but they are irrelevant + // when searching with [find.byType]. + final Type radioType = const Radio( + value: 0, + groupValue: 0, + onChanged: null, + ).runtimeType; + final List log = []; + + Widget buildFrame() { + return wrap( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Scaffold( + body: ListView.builder( + itemCount: values.length, + itemBuilder: (BuildContext context, int index) => RadioListTile( + onChanged: (int value) { + log.add(value); + setState(() { selectedValue = value; }); + }, + value: values[index], + groupValue: selectedValue, + title: Text(values[index].toString()), + ), + ), + ); + }, + ), + ); + } + + await tester.pumpWidget(buildFrame()); + await tester.tap(find.text('0')); + await tester.pump(); + expect(log, equals([0])); + + await tester.tap(find.text('0')); + expect(log, equals([0])); + + await tester.tap(find.byType(radioType).at(0)); + await tester.pump(); + expect(log, equals([0])); }); testWidgets('SwitchListTile control test', (WidgetTester tester) async {