// Copyright 2014 The Flutter 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/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; class TestIcon extends StatefulWidget { const TestIcon({ super.key }); @override TestIconState createState() => TestIconState(); } class TestIconState extends State<TestIcon> { late IconThemeData iconTheme; @override Widget build(BuildContext context) { iconTheme = IconTheme.of(context); return const Icon(Icons.add); } } class TestText extends StatefulWidget { const TestText(this.text, { super.key }); final String text; @override TestTextState createState() => TestTextState(); } class TestTextState extends State<TestText> { late TextStyle textStyle; @override Widget build(BuildContext context) { textStyle = DefaultTextStyle.of(context).style; return Text(widget.text); } } void main() { test('ListTileThemeData copyWith, ==, hashCode, basics', () { expect(const ListTileThemeData(), const ListTileThemeData().copyWith()); expect(const ListTileThemeData().hashCode, const ListTileThemeData().copyWith().hashCode); }); test('ListTileThemeData lerp special cases', () { expect(ListTileThemeData.lerp(null, null, 0), null); const ListTileThemeData data = ListTileThemeData(); expect(identical(ListTileThemeData.lerp(data, data, 0.5), data), true); }); test('ListTileThemeData defaults', () { const ListTileThemeData themeData = ListTileThemeData(); expect(themeData.dense, null); expect(themeData.shape, null); expect(themeData.style, null); expect(themeData.selectedColor, null); expect(themeData.iconColor, null); expect(themeData.textColor, null); expect(themeData.titleTextStyle, null); expect(themeData.subtitleTextStyle, null); expect(themeData.leadingAndTrailingTextStyle, null); expect(themeData.contentPadding, null); expect(themeData.tileColor, null); expect(themeData.selectedTileColor, null); expect(themeData.horizontalTitleGap, null); expect(themeData.minVerticalPadding, null); expect(themeData.minLeadingWidth, null); expect(themeData.minTileHeight, null); expect(themeData.enableFeedback, null); expect(themeData.mouseCursor, null); expect(themeData.visualDensity, null); expect(themeData.titleAlignment, null); }); testWidgets('Default ListTileThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ListTileThemeData().debugFillProperties(builder); final List<String> description = builder.properties .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) .map((DiagnosticsNode node) => node.toString()) .toList(); expect(description, <String>[]); }); testWidgets('ListTileThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ListTileThemeData( dense: true, shape: StadiumBorder(), style: ListTileStyle.drawer, selectedColor: Color(0x00000001), iconColor: Color(0x00000002), textColor: Color(0x00000003), titleTextStyle: TextStyle(color: Color(0x00000004)), subtitleTextStyle: TextStyle(color: Color(0x00000005)), leadingAndTrailingTextStyle: TextStyle(color: Color(0x00000006)), contentPadding: EdgeInsets.all(100), tileColor: Color(0x00000007), selectedTileColor: Color(0x00000008), horizontalTitleGap: 200, minVerticalPadding: 300, minLeadingWidth: 400, minTileHeight: 30, enableFeedback: true, mouseCursor: MaterialStateMouseCursor.clickable, visualDensity: VisualDensity.comfortable, titleAlignment: ListTileTitleAlignment.top, ).debugFillProperties(builder); final List<String> description = builder.properties .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) .map((DiagnosticsNode node) => node.toString()) .toList(); expect( description, equalsIgnoringHashCodes(<String>[ 'dense: true', 'shape: StadiumBorder(BorderSide(width: 0.0, style: none))', 'style: drawer', 'selectedColor: Color(0x00000001)', 'iconColor: Color(0x00000002)', 'textColor: Color(0x00000003)', 'titleTextStyle: TextStyle(inherit: true, color: Color(0x00000004))', 'subtitleTextStyle: TextStyle(inherit: true, color: Color(0x00000005))', 'leadingAndTrailingTextStyle: TextStyle(inherit: true, color: Color(0x00000006))', 'contentPadding: EdgeInsets.all(100.0)', 'tileColor: Color(0x00000007)', 'selectedTileColor: Color(0x00000008)', 'horizontalTitleGap: 200.0', 'minVerticalPadding: 300.0', 'minLeadingWidth: 400.0', 'minTileHeight: 30.0', 'enableFeedback: true', 'mouseCursor: WidgetStateMouseCursor(clickable)', 'visualDensity: VisualDensity#00000(h: -1.0, v: -1.0)(horizontal: -1.0, vertical: -1.0)', 'titleAlignment: ListTileTitleAlignment.top', ]), ); }); testWidgets('ListTileTheme backwards compatibility constructor', (WidgetTester tester) async { late ListTileThemeData theme; await tester.pumpWidget( MaterialApp( home: Material( child: ListTileTheme( dense: true, shape: const StadiumBorder(), style: ListTileStyle.drawer, selectedColor: const Color(0x00000001), iconColor: const Color(0x00000002), textColor: const Color(0x00000003), contentPadding: const EdgeInsets.all(100), tileColor: const Color(0x00000004), selectedTileColor: const Color(0x00000005), horizontalTitleGap: 200, minVerticalPadding: 300, minLeadingWidth: 400, enableFeedback: true, mouseCursor: MaterialStateMouseCursor.clickable, child: Center( child: Builder( builder: (BuildContext context) { theme = ListTileTheme.of(context); return const Placeholder(); }, ), ), ), ), ), ); expect(theme.dense, true); expect(theme.shape, const StadiumBorder()); expect(theme.style, ListTileStyle.drawer); expect(theme.selectedColor, const Color(0x00000001)); expect(theme.iconColor, const Color(0x00000002)); expect(theme.textColor, const Color(0x00000003)); expect(theme.contentPadding, const EdgeInsets.all(100)); expect(theme.tileColor, const Color(0x00000004)); expect(theme.selectedTileColor, const Color(0x00000005)); expect(theme.horizontalTitleGap, 200); expect(theme.minVerticalPadding, 300); expect(theme.minLeadingWidth, 400); expect(theme.enableFeedback, true); expect(theme.mouseCursor, MaterialStateMouseCursor.clickable); }); testWidgets('ListTileTheme', (WidgetTester tester) async { final Key listTileKey = UniqueKey(); final Key titleKey = UniqueKey(); final Key subtitleKey = UniqueKey(); final Key leadingKey = UniqueKey(); final Key trailingKey = UniqueKey(); late ThemeData theme; Widget buildFrame({ bool enabled = true, bool dense = false, bool selected = false, ShapeBorder? shape, Color? selectedColor, Color? iconColor, Color? textColor, }) { return MaterialApp( theme: ThemeData(useMaterial3: false), home: Material( child: Center( child: ListTileTheme( data: ListTileThemeData( dense: dense, shape: shape, selectedColor: selectedColor, iconColor: iconColor, textColor: textColor, minVerticalPadding: 25.0, mouseCursor: MaterialStateProperty.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return SystemMouseCursors.forbidden; } return SystemMouseCursors.click; }), visualDensity: VisualDensity.compact, titleAlignment: ListTileTitleAlignment.bottom, ), child: Builder( builder: (BuildContext context) { theme = Theme.of(context); return ListTile( key: listTileKey, enabled: enabled, selected: selected, leading: TestIcon(key: leadingKey), trailing: TestIcon(key: trailingKey), title: TestText('title', key: titleKey), subtitle: TestText('subtitle', key: subtitleKey), ); }, ), ), ), ), ); } const Color green = Color(0xFF00FF00); const Color red = Color(0xFFFF0000); const ShapeBorder roundedShape = RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(4.0)), ); Color iconColor(Key key) => tester.state<TestIconState>(find.byKey(key)).iconTheme.color!; Color textColor(Key key) => tester.state<TestTextState>(find.byKey(key)).textStyle.color!; ShapeBorder inkWellBorder() => tester.widget<InkWell>(find.descendant(of: find.byType(ListTile), matching: find.byType(InkWell))).customBorder!; // A selected ListTile's leading, trailing, and text get the primary color by default await tester.pumpWidget(buildFrame(selected: true)); await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate expect(iconColor(leadingKey), theme.primaryColor); expect(iconColor(trailingKey), theme.primaryColor); expect(textColor(titleKey), theme.primaryColor); expect(textColor(subtitleKey), theme.primaryColor); // A selected ListTile's leading, trailing, and text get the ListTileTheme's selectedColor await tester.pumpWidget(buildFrame(selected: true, selectedColor: green)); await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate expect(iconColor(leadingKey), green); expect(iconColor(trailingKey), green); expect(textColor(titleKey), green); expect(textColor(subtitleKey), green); // An unselected ListTile's leading and trailing get the ListTileTheme's iconColor // An unselected ListTile's title texts get the ListTileTheme's textColor await tester.pumpWidget(buildFrame(iconColor: red, textColor: green)); await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate expect(iconColor(leadingKey), red); expect(iconColor(trailingKey), red); expect(textColor(titleKey), green); expect(textColor(subtitleKey), green); // If the item is disabled it's rendered with the theme's disabled color. await tester.pumpWidget(buildFrame(enabled: false)); await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate expect(iconColor(leadingKey), theme.disabledColor); expect(iconColor(trailingKey), theme.disabledColor); expect(textColor(titleKey), theme.disabledColor); expect(textColor(subtitleKey), theme.disabledColor); // If the item is disabled it's rendered with the theme's disabled color. // Even if it's selected. await tester.pumpWidget(buildFrame(enabled: false, selected: true)); await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate expect(iconColor(leadingKey), theme.disabledColor); expect(iconColor(trailingKey), theme.disabledColor); expect(textColor(titleKey), theme.disabledColor); expect(textColor(subtitleKey), theme.disabledColor); // A selected ListTile's InkWell gets the ListTileTheme's shape await tester.pumpWidget(buildFrame(selected: true, shape: roundedShape)); expect(inkWellBorder(), roundedShape); // Cursor updates when hovering disabled ListTile await tester.pumpWidget(buildFrame(enabled: false)); final Offset listTile = tester.getCenter(find.byKey(titleKey)); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.moveTo(listTile); await tester.pumpAndSettle(); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); // VisualDensity is respected final RenderBox box = tester.renderObject(find.byKey(listTileKey)); expect(box.size, equals(const Size(800, 80.0))); // titleAlignment is respected. final Offset titleOffset = tester.getTopLeft(find.text('title')); final Offset leadingOffset = tester.getTopLeft(find.byKey(leadingKey)); final Offset trailingOffset = tester.getTopRight(find.byKey(trailingKey)); expect(leadingOffset.dy - titleOffset.dy, 6); expect(trailingOffset.dy - titleOffset.dy, 6); }); testWidgets('ListTileTheme colors are applied to leading and trailing text widgets', (WidgetTester tester) async { final Key leadingKey = UniqueKey(); final Key trailingKey = UniqueKey(); const Color selectedColor = Colors.orange; const Color defaultColor = Colors.black; late ThemeData theme; Widget buildFrame({ bool enabled = true, bool selected = false, }) { return MaterialApp( home: Material( child: Center( child: ListTileTheme( data: const ListTileThemeData( selectedColor: selectedColor, textColor: defaultColor, ), child: Builder( builder: (BuildContext context) { theme = Theme.of(context); return ListTile( enabled: enabled, selected: selected, leading: TestText('leading', key: leadingKey), title: const TestText('title'), trailing: TestText('trailing', key: trailingKey), ); }, ), ), ), ), ); } Color textColor(Key key) => tester.state<TestTextState>(find.byKey(key)).textStyle.color!; await tester.pumpWidget(buildFrame()); // Enabled color should use ListTileTheme.textColor. expect(textColor(leadingKey), defaultColor); expect(textColor(trailingKey), defaultColor); await tester.pumpWidget(buildFrame(selected: true)); // Wait for text color to animate. await tester.pumpAndSettle(); // Selected color should use ListTileTheme.selectedColor. expect(textColor(leadingKey), selectedColor); expect(textColor(trailingKey), selectedColor); await tester.pumpWidget(buildFrame(enabled: false)); // Wait for text color to animate. await tester.pumpAndSettle(); // Disabled color should be ThemeData.disabledColor. expect(textColor(leadingKey), theme.disabledColor); expect(textColor(trailingKey), theme.disabledColor); }); testWidgets( "Material3 - ListTile respects ListTileTheme's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle", (WidgetTester tester) async { const TextStyle titleTextStyle = TextStyle( fontSize: 23.0, color: Color(0xffff0000), fontStyle: FontStyle.italic, ); const TextStyle subtitleTextStyle = TextStyle( fontSize: 20.0, color: Color(0xff00ff00), fontStyle: FontStyle.italic, ); const TextStyle leadingAndTrailingTextStyle = TextStyle( fontSize: 18.0, color: Color(0xff0000ff), fontStyle: FontStyle.italic, ); final ThemeData theme = ThemeData( useMaterial3: true, listTileTheme: const ListTileThemeData( titleTextStyle: titleTextStyle, subtitleTextStyle: subtitleTextStyle, leadingAndTrailingTextStyle: leadingAndTrailingTextStyle, ), ); Widget buildFrame() { return MaterialApp( theme: theme, home: Material( child: Center( child: Builder( builder: (BuildContext context) { return const ListTile( leading: TestText('leading'), title: TestText('title'), subtitle: TestText('subtitle'), trailing: TestText('trailing'), ); }, ), ), ), ); } await tester.pumpWidget(buildFrame()); final RenderParagraph leading = _getTextRenderObject(tester, 'leading'); expect(leading.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize); expect(leading.text.style!.color, leadingAndTrailingTextStyle.color); expect(leading.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle); final RenderParagraph title = _getTextRenderObject(tester, 'title'); expect(title.text.style!.fontSize, titleTextStyle.fontSize); expect(title.text.style!.color, titleTextStyle.color); expect(title.text.style!.fontStyle, titleTextStyle.fontStyle); final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle'); expect(subtitle.text.style!.fontSize, subtitleTextStyle.fontSize); expect(subtitle.text.style!.color, subtitleTextStyle.color); expect(subtitle.text.style!.fontStyle, subtitleTextStyle.fontStyle); final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing'); expect(trailing.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize); expect(trailing.text.style!.color, leadingAndTrailingTextStyle.color); expect(trailing.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle); }); testWidgets( "Material2 - ListTile respects ListTileTheme's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle", (WidgetTester tester) async { const TextStyle titleTextStyle = TextStyle( fontSize: 23.0, color: Color(0xffff0000), fontStyle: FontStyle.italic, ); const TextStyle subtitleTextStyle = TextStyle( fontSize: 20.0, color: Color(0xff00ff00), fontStyle: FontStyle.italic, ); const TextStyle leadingAndTrailingTextStyle = TextStyle( fontSize: 18.0, color: Color(0xff0000ff), fontStyle: FontStyle.italic, ); final ThemeData theme = ThemeData( useMaterial3: false, listTileTheme: const ListTileThemeData( titleTextStyle: titleTextStyle, subtitleTextStyle: subtitleTextStyle, leadingAndTrailingTextStyle: leadingAndTrailingTextStyle, ), ); Widget buildFrame() { return MaterialApp( theme: theme, home: Material( child: Center( child: Builder( builder: (BuildContext context) { return const ListTile( leading: TestText('leading'), title: TestText('title'), subtitle: TestText('subtitle'), trailing: TestText('trailing'), ); }, ), ), ), ); } await tester.pumpWidget(buildFrame()); final RenderParagraph leading = _getTextRenderObject(tester, 'leading'); expect(leading.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize); expect(leading.text.style!.color, leadingAndTrailingTextStyle.color); expect(leading.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle); final RenderParagraph title = _getTextRenderObject(tester, 'title'); expect(title.text.style!.fontSize, titleTextStyle.fontSize); expect(title.text.style!.color, titleTextStyle.color); expect(title.text.style!.fontStyle, titleTextStyle.fontStyle); final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle'); expect(subtitle.text.style!.fontSize, subtitleTextStyle.fontSize); expect(subtitle.text.style!.color, subtitleTextStyle.color); expect(subtitle.text.style!.fontStyle, subtitleTextStyle.fontStyle); final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing'); expect(trailing.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize); expect(trailing.text.style!.color, leadingAndTrailingTextStyle.color); expect(trailing.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle); }); testWidgets( "Material3 - ListTile's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle are overridden by ListTile properties", (WidgetTester tester) async { final ThemeData theme = ThemeData( useMaterial3: true, listTileTheme: const ListTileThemeData( titleTextStyle: TextStyle(fontSize: 20.0), subtitleTextStyle: TextStyle(fontSize: 17.5), leadingAndTrailingTextStyle: TextStyle(fontSize: 15.0), ), ); const TextStyle titleTextStyle = TextStyle( fontSize: 23.0, color: Color(0xffff0000), fontStyle: FontStyle.italic, ); const TextStyle subtitleTextStyle = TextStyle( fontSize: 20.0, color: Color(0xff00ff00), fontStyle: FontStyle.italic, ); const TextStyle leadingAndTrailingTextStyle = TextStyle( fontSize: 18.0, color: Color(0xff0000ff), fontStyle: FontStyle.italic, ); Widget buildFrame() { return MaterialApp( theme: theme, home: Material( child: Center( child: Builder( builder: (BuildContext context) { return const ListTile( titleTextStyle: titleTextStyle, subtitleTextStyle: subtitleTextStyle, leadingAndTrailingTextStyle: leadingAndTrailingTextStyle, leading: TestText('leading'), title: TestText('title'), subtitle: TestText('subtitle'), trailing: TestText('trailing'), ); }, ), ), ), ); } await tester.pumpWidget(buildFrame()); final RenderParagraph leading = _getTextRenderObject(tester, 'leading'); expect(leading.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize); expect(leading.text.style!.color, leadingAndTrailingTextStyle.color); expect(leading.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle); final RenderParagraph title = _getTextRenderObject(tester, 'title'); expect(title.text.style!.fontSize, titleTextStyle.fontSize); expect(title.text.style!.color, titleTextStyle.color); expect(title.text.style!.fontStyle, titleTextStyle.fontStyle); final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle'); expect(subtitle.text.style!.fontSize, subtitleTextStyle.fontSize); expect(subtitle.text.style!.color, subtitleTextStyle.color); expect(subtitle.text.style!.fontStyle, subtitleTextStyle.fontStyle); final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing'); expect(trailing.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize); expect(trailing.text.style!.color, leadingAndTrailingTextStyle.color); expect(trailing.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle); }); testWidgets( "Material2 - ListTile's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle are overridden by ListTile properties", (WidgetTester tester) async { final ThemeData theme = ThemeData( useMaterial3: false, listTileTheme: const ListTileThemeData( titleTextStyle: TextStyle(fontSize: 20.0), subtitleTextStyle: TextStyle(fontSize: 17.5), leadingAndTrailingTextStyle: TextStyle(fontSize: 15.0), ), ); const TextStyle titleTextStyle = TextStyle( fontSize: 23.0, color: Color(0xffff0000), fontStyle: FontStyle.italic, ); const TextStyle subtitleTextStyle = TextStyle( fontSize: 20.0, color: Color(0xff00ff00), fontStyle: FontStyle.italic, ); const TextStyle leadingAndTrailingTextStyle = TextStyle( fontSize: 18.0, color: Color(0xff0000ff), fontStyle: FontStyle.italic, ); Widget buildFrame() { return MaterialApp( theme: theme, home: Material( child: Center( child: Builder( builder: (BuildContext context) { return const ListTile( titleTextStyle: titleTextStyle, subtitleTextStyle: subtitleTextStyle, leadingAndTrailingTextStyle: leadingAndTrailingTextStyle, leading: TestText('leading'), title: TestText('title'), subtitle: TestText('subtitle'), trailing: TestText('trailing'), ); }, ), ), ), ); } await tester.pumpWidget(buildFrame()); final RenderParagraph leading = _getTextRenderObject(tester, 'leading'); expect(leading.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize); expect(leading.text.style!.color, leadingAndTrailingTextStyle.color); expect(leading.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle); final RenderParagraph title = _getTextRenderObject(tester, 'title'); expect(title.text.style!.fontSize, titleTextStyle.fontSize); expect(title.text.style!.color, titleTextStyle.color); expect(title.text.style!.fontStyle, titleTextStyle.fontStyle); final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle'); expect(subtitle.text.style!.fontSize, subtitleTextStyle.fontSize); expect(subtitle.text.style!.color, subtitleTextStyle.color); expect(subtitle.text.style!.fontStyle, subtitleTextStyle.fontStyle); final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing'); expect(trailing.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize); expect(trailing.text.style!.color, leadingAndTrailingTextStyle.color); expect(trailing.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle); }); testWidgets("ListTile respects ListTileTheme's tileColor & selectedTileColor", (WidgetTester tester) async { late ListTileThemeData theme; bool isSelected = false; await tester.pumpWidget( MaterialApp( home: Material( child: ListTileTheme( data: ListTileThemeData( tileColor: Colors.green.shade500, selectedTileColor: Colors.red.shade500, ), child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { theme = ListTileTheme.of(context); return ListTile( selected: isSelected, onTap: () { setState(()=> isSelected = !isSelected); }, title: const Text('Title'), ); }, ), ), ), ), ), ); expect(find.byType(Material), paints..rect(color: theme.tileColor)); // Tap on tile to change isSelected. await tester.tap(find.byType(ListTile)); await tester.pumpAndSettle(); expect(find.byType(Material), paints..rect(color: theme.selectedTileColor)); }); testWidgets("ListTileTheme's tileColor & selectedTileColor are overridden by ListTile properties", (WidgetTester tester) async { bool isSelected = false; final Color tileColor = Colors.green.shade500; final Color selectedTileColor = Colors.red.shade500; await tester.pumpWidget( MaterialApp( home: Material( child: ListTileTheme( data: const ListTileThemeData( selectedTileColor: Colors.green, tileColor: Colors.red, ), child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return ListTile( tileColor: tileColor, selectedTileColor: selectedTileColor, selected: isSelected, onTap: () { setState(()=> isSelected = !isSelected); }, title: const Text('Title'), ); }, ), ), ), ), ), ); expect(find.byType(Material), paints..rect(color: tileColor)); // Tap on tile to change isSelected. await tester.tap(find.byType(ListTile)); await tester.pumpAndSettle(); expect(find.byType(Material), paints..rect(color: selectedTileColor)); }); testWidgets('ListTile uses ListTileTheme shape in a drawer', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/106303 final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); final ShapeBorder shapeBorder = RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)); await tester.pumpWidget(MaterialApp( theme: ThemeData( listTileTheme: ListTileThemeData(shape: shapeBorder), ), home: Scaffold( key: scaffoldKey, drawer: const Drawer( child: ListTile(), ), body: Container(), ), )); await tester.pumpAndSettle(); scaffoldKey.currentState!.openDrawer(); // Start drawer animation. await tester.pump(); final ShapeBorder? inkWellBorder = tester.widget<InkWell>( find.descendant( of: find.byType(ListTile), matching: find.byType(InkWell), )).customBorder; // Test shape. expect(inkWellBorder, shapeBorder); }); testWidgets('ListTile respects MaterialStateColor LisTileTheme.textColor', (WidgetTester tester) async { bool enabled = false; bool selected = false; const Color defaultColor = Colors.blue; const Color selectedColor = Colors.green; const Color disabledColor = Colors.red; final ThemeData theme = ThemeData( listTileTheme: ListTileThemeData( textColor: MaterialStateColor.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return disabledColor; } if (states.contains(MaterialState.selected)) { return selectedColor; } return defaultColor; }), ), ); Widget buildFrame() { return MaterialApp( theme: theme, home: Material( child: Center( child: Builder( builder: (BuildContext context) { return ListTile( enabled: enabled, selected: selected, title: const TestText('title'), subtitle: const TestText('subtitle') , ); }, ), ), ), ); } // Test disabled state. await tester.pumpWidget(buildFrame()); RenderParagraph title = _getTextRenderObject(tester, 'title'); expect(title.text.style!.color, disabledColor); // Test enabled state. enabled = true; await tester.pumpWidget(buildFrame()); await tester.pumpAndSettle(); title = _getTextRenderObject(tester, 'title'); expect(title.text.style!.color, defaultColor); // Test selected state. selected = true; await tester.pumpWidget(buildFrame()); await tester.pumpAndSettle(); title = _getTextRenderObject(tester, 'title'); expect(title.text.style!.color, selectedColor); }); testWidgets('ListTile respects MaterialStateColor LisTileTheme.iconColor', (WidgetTester tester) async { bool enabled = false; bool selected = false; const Color defaultColor = Colors.blue; const Color selectedColor = Colors.green; const Color disabledColor = Colors.red; final Key leadingKey = UniqueKey(); final ThemeData theme = ThemeData( listTileTheme: ListTileThemeData( iconColor: MaterialStateColor.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return disabledColor; } if (states.contains(MaterialState.selected)) { return selectedColor; } return defaultColor; }), ), ); Widget buildFrame() { return MaterialApp( theme: theme, home: Material( child: Center( child: Builder( builder: (BuildContext context) { return ListTile( enabled: enabled, selected: selected, leading: TestIcon(key: leadingKey), ); }, ), ), ), ); } Color iconColor(Key key) => tester.state<TestIconState>(find.byKey(key)).iconTheme.color!; // Test disabled state. await tester.pumpWidget(buildFrame()); expect(iconColor(leadingKey), disabledColor); // Test enabled state. enabled = true; await tester.pumpWidget(buildFrame()); await tester.pumpAndSettle(); expect(iconColor(leadingKey), defaultColor); // Test selected state. selected = true; await tester.pumpWidget(buildFrame()); await tester.pumpAndSettle(); expect(iconColor(leadingKey), selectedColor); }); testWidgets('ListTileThemeData copyWith overrides all properties', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/119734 const ListTileThemeData original = ListTileThemeData( dense: true, shape: StadiumBorder(), style: ListTileStyle.drawer, selectedColor: Color(0x00000001), iconColor: Color(0x00000002), textColor: Color(0x00000003), titleTextStyle: TextStyle(color: Color(0x00000004)), subtitleTextStyle: TextStyle(color: Color(0x00000005)), leadingAndTrailingTextStyle: TextStyle(color: Color(0x00000006)), contentPadding: EdgeInsets.all(100), tileColor: Color(0x00000007), selectedTileColor: Color(0x00000008), horizontalTitleGap: 200, minVerticalPadding: 300, minLeadingWidth: 400, minTileHeight: 30, enableFeedback: true, titleAlignment: ListTileTitleAlignment.bottom, ); final ListTileThemeData copy = original.copyWith( dense: false, shape: const RoundedRectangleBorder(), style: ListTileStyle.list, selectedColor: const Color(0x00000009), iconColor: const Color(0x0000000A), textColor: const Color(0x0000000B), titleTextStyle: const TextStyle(color: Color(0x0000000C)), subtitleTextStyle: const TextStyle(color: Color(0x0000000D)), leadingAndTrailingTextStyle: const TextStyle(color: Color(0x0000000E)), contentPadding: const EdgeInsets.all(500), tileColor: const Color(0x0000000F), selectedTileColor: const Color(0x00000010), horizontalTitleGap: 600, minVerticalPadding: 700, minLeadingWidth: 800, minTileHeight: 80, enableFeedback: false, titleAlignment: ListTileTitleAlignment.top, ); expect(copy.dense, false); expect(copy.shape, const RoundedRectangleBorder()); expect(copy.style, ListTileStyle.list); expect(copy.selectedColor, const Color(0x00000009)); expect(copy.iconColor, const Color(0x0000000A)); expect(copy.textColor, const Color(0x0000000B)); expect(copy.titleTextStyle, const TextStyle(color: Color(0x0000000C))); expect(copy.subtitleTextStyle, const TextStyle(color: Color(0x0000000D))); expect(copy.leadingAndTrailingTextStyle, const TextStyle(color: Color(0x0000000E))); expect(copy.contentPadding, const EdgeInsets.all(500)); expect(copy.tileColor, const Color(0x0000000F)); expect(copy.selectedTileColor, const Color(0x00000010)); expect(copy.horizontalTitleGap, 600); expect(copy.minVerticalPadding, 700); expect(copy.minLeadingWidth, 800); expect(copy.minTileHeight, 80); expect(copy.enableFeedback, false); expect(copy.titleAlignment, ListTileTitleAlignment.top); }); testWidgets('ListTileTheme.titleAlignment is overridden by ListTile.titleAlignment', (WidgetTester tester) async { final Key leadingKey = GlobalKey(); final Key trailingKey = GlobalKey(); const String titleText = '\nHeadline Text\n'; const String subtitleText = '\nSupporting Text\n'; Widget buildFrame({ ListTileTitleAlignment? alignment }) { return MaterialApp( theme: ThemeData( useMaterial3: true, listTileTheme: const ListTileThemeData( titleAlignment: ListTileTitleAlignment.center, ), ), home: Material( child: Center( child: ListTile( titleAlignment: ListTileTitleAlignment.top, leading: SizedBox(key: leadingKey, width: 24.0, height: 24.0), title: const Text(titleText), subtitle: const Text(subtitleText), trailing: SizedBox(key: trailingKey, width: 24.0, height: 24.0), ), ), ), ); } await tester.pumpWidget(buildFrame()); final Offset tileOffset = tester.getTopLeft(find.byType(ListTile)); final Offset leadingOffset = tester.getTopLeft(find.byKey(leadingKey)); final Offset trailingOffset = tester.getTopRight(find.byKey(trailingKey)); expect(leadingOffset.dy - tileOffset.dy, 8.0); expect(trailingOffset.dy - tileOffset.dy, 8.0); }); testWidgets('ListTileTheme.merge supports all properties', (WidgetTester tester) async { Widget buildFrame() { return MaterialApp( theme: ThemeData( listTileTheme: const ListTileThemeData( dense: true, shape: StadiumBorder(), style: ListTileStyle.drawer, selectedColor: Color(0x00000001), iconColor: Color(0x00000002), textColor: Color(0x00000003), titleTextStyle: TextStyle(color: Color(0x00000004)), subtitleTextStyle: TextStyle(color: Color(0x00000005)), leadingAndTrailingTextStyle: TextStyle(color: Color(0x00000006)), contentPadding: EdgeInsets.all(100), tileColor: Color(0x00000007), selectedTileColor: Color(0x00000008), horizontalTitleGap: 200, minVerticalPadding: 300, minLeadingWidth: 400, minTileHeight: 30, enableFeedback: true, titleAlignment: ListTileTitleAlignment.bottom, mouseCursor: MaterialStateMouseCursor.textable, visualDensity: VisualDensity.comfortable, ), ), home: Material( child: Center( child: Builder( builder: (BuildContext context) { return ListTileTheme.merge( dense: false, shape: const RoundedRectangleBorder(), style: ListTileStyle.list, selectedColor: const Color(0x00000009), iconColor: const Color(0x0000000A), textColor: const Color(0x0000000B), titleTextStyle: const TextStyle(color: Color(0x0000000C)), subtitleTextStyle: const TextStyle(color: Color(0x0000000D)), leadingAndTrailingTextStyle: const TextStyle(color: Color(0x0000000E)), contentPadding: const EdgeInsets.all(500), tileColor: const Color(0x0000000F), selectedTileColor: const Color(0x00000010), horizontalTitleGap: 600, minVerticalPadding: 700, minLeadingWidth: 800, minTileHeight: 80, enableFeedback: false, titleAlignment: ListTileTitleAlignment.top, mouseCursor: MaterialStateMouseCursor.clickable, visualDensity: VisualDensity.compact, child: const ListTile(), ); } ), ), ), ); } await tester.pumpWidget(buildFrame()); final ListTileThemeData theme = ListTileTheme.of(tester.element(find.byType(ListTile))); expect(theme.dense, false); expect(theme.shape, const RoundedRectangleBorder()); expect(theme.style, ListTileStyle.list); expect(theme.selectedColor, const Color(0x00000009)); expect(theme.iconColor, const Color(0x0000000A)); expect(theme.textColor, const Color(0x0000000B)); expect(theme.titleTextStyle, const TextStyle(color: Color(0x0000000C))); expect(theme.subtitleTextStyle, const TextStyle(color: Color(0x0000000D))); expect(theme.leadingAndTrailingTextStyle, const TextStyle(color: Color(0x0000000E))); expect(theme.contentPadding, const EdgeInsets.all(500)); expect(theme.tileColor, const Color(0x0000000F)); expect(theme.selectedTileColor, const Color(0x00000010)); expect(theme.horizontalTitleGap, 600); expect(theme.minVerticalPadding, 700); expect(theme.minLeadingWidth, 800); expect(theme.minTileHeight, 80); expect(theme.enableFeedback, false); expect(theme.titleAlignment, ListTileTitleAlignment.top); expect(theme.mouseCursor, MaterialStateMouseCursor.clickable); expect(theme.visualDensity, VisualDensity.compact); }); } RenderParagraph _getTextRenderObject(WidgetTester tester, String text) { return tester.renderObject(find.descendant( of: find.byType(ListTile), matching: find.text(text), )); }