forked from flutter/packages
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ListenableBuilder with examples (#116543)
* Add ListenableBuilder with examples * Add tests * Add tests * Fix Test * Change AnimatedBuilder to be a subclass of ListenableBuilder
- Loading branch information
1 parent
609fe35
commit fb9133b
Showing
9 changed files
with
557 additions
and
104 deletions.
There are no files selected for viewing
172 changes: 172 additions & 0 deletions
172
examples/api/lib/widgets/transitions/listenable_builder.0.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
// 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. | ||
|
||
/// Flutter code sample for [ListenableBuilder]. | ||
import 'package:flutter/material.dart'; | ||
|
||
void main() => runApp(const ListenableBuilderExample()); | ||
|
||
/// This widget listens for changes in the focus state of the subtree defined by | ||
/// its [child] widget, changing the border and color of the container it is in | ||
/// when it has focus. | ||
/// | ||
/// A [FocusListenerContainer] swaps out the [BorderSide] of a border around the | ||
/// child widget with [focusedSide], and the background color with | ||
/// [focusedColor], when a widget that is a descendant of this widget has focus. | ||
class FocusListenerContainer extends StatefulWidget { | ||
const FocusListenerContainer({ | ||
super.key, | ||
this.border, | ||
this.padding, | ||
this.focusedSide, | ||
this.focusedColor = Colors.black12, | ||
required this.child, | ||
}); | ||
|
||
/// This is the border that will be used when not focused, and which defines | ||
/// all the attributes except for the [OutlinedBorder.side] when focused. | ||
final OutlinedBorder? border; | ||
|
||
/// This is the [BorderSide] that will be used for [border] when the [child] | ||
/// subtree is focused. | ||
final BorderSide? focusedSide; | ||
|
||
/// This is the [Color] that will be used as the fill color for the background | ||
/// of the [child] when a descendant widget is focused. | ||
final Color? focusedColor; | ||
|
||
/// The padding around the inside of the container. | ||
final EdgeInsetsGeometry? padding; | ||
|
||
/// This is defines the subtree to listen to for focus changes. | ||
final Widget child; | ||
|
||
@override | ||
State<FocusListenerContainer> createState() => _FocusListenerContainerState(); | ||
} | ||
|
||
class _FocusListenerContainerState extends State<FocusListenerContainer> { | ||
final FocusNode _focusNode = FocusNode(); | ||
|
||
@override | ||
void dispose() { | ||
_focusNode.dispose(); | ||
super.dispose(); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
final OutlinedBorder effectiveBorder = widget.border ?? const RoundedRectangleBorder(); | ||
return ListenableBuilder( | ||
listenable: _focusNode, | ||
child: Focus( | ||
focusNode: _focusNode, | ||
skipTraversal: true, | ||
canRequestFocus: false, | ||
child: widget.child, | ||
), | ||
builder: (BuildContext context, Widget? child) { | ||
return Container( | ||
padding: widget.padding, | ||
decoration: ShapeDecoration( | ||
color: _focusNode.hasFocus ? widget.focusedColor : null, | ||
shape: effectiveBorder.copyWith( | ||
side: _focusNode.hasFocus ? widget.focusedSide : null, | ||
), | ||
), | ||
child: child, | ||
); | ||
}, | ||
); | ||
} | ||
} | ||
|
||
class MyField extends StatefulWidget { | ||
const MyField({super.key, required this.label}); | ||
|
||
final String label; | ||
|
||
@override | ||
State<MyField> createState() => _MyFieldState(); | ||
} | ||
|
||
class _MyFieldState extends State<MyField> { | ||
final TextEditingController controller = TextEditingController(); | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return Row( | ||
children: <Widget>[ | ||
Expanded(child: Text(widget.label)), | ||
Expanded( | ||
flex: 2, | ||
child: TextField( | ||
controller: controller, | ||
onEditingComplete: () { | ||
debugPrint('Field ${widget.label} changed to ${controller.value}'); | ||
}, | ||
), | ||
), | ||
], | ||
); | ||
} | ||
} | ||
|
||
class ListenableBuilderExample extends StatelessWidget { | ||
const ListenableBuilderExample({super.key}); | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return MaterialApp( | ||
home: Scaffold( | ||
appBar: AppBar(title: const Text('ListenableBuilder Example')), | ||
body: Center( | ||
child: SizedBox( | ||
width: 300, | ||
child: Padding( | ||
padding: const EdgeInsets.all(8.0), | ||
child: Column( | ||
mainAxisAlignment: MainAxisAlignment.center, | ||
children: <Widget>[ | ||
const Padding( | ||
padding: EdgeInsets.only(bottom: 8), | ||
child: MyField(label: 'Company'), | ||
), | ||
FocusListenerContainer( | ||
padding: const EdgeInsets.all(8), | ||
border: const RoundedRectangleBorder( | ||
side: BorderSide( | ||
strokeAlign: BorderSide.strokeAlignOutside, | ||
), | ||
borderRadius: BorderRadius.all( | ||
Radius.circular(5), | ||
), | ||
), | ||
// The border side will get wider when the subtree has focus. | ||
focusedSide: const BorderSide( | ||
width: 4, | ||
strokeAlign: BorderSide.strokeAlignOutside, | ||
), | ||
// The container background will change color to this when | ||
// the subtree has focus. | ||
focusedColor: Colors.blue.shade50, | ||
child: Column( | ||
crossAxisAlignment: CrossAxisAlignment.start, | ||
children: const <Widget>[ | ||
Text('Owner:'), | ||
MyField(label: 'First Name'), | ||
MyField(label: 'Last Name'), | ||
], | ||
), | ||
), | ||
], | ||
), | ||
), | ||
), | ||
), | ||
), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 0 additions & 33 deletions
33
examples/api/test/foundation/change_notifier/change_notifier.0_test.dart
This file was deleted.
Oops, something went wrong.
43 changes: 43 additions & 0 deletions
43
examples/api/test/widgets/transitions/listenable_builder.0_test.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
// 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/material.dart'; | ||
import 'package:flutter_api_samples/widgets/transitions/listenable_builder.0.dart' as example; | ||
import 'package:flutter_test/flutter_test.dart'; | ||
|
||
void main() { | ||
testWidgets('Changing focus changes border', (WidgetTester tester) async { | ||
await tester.pumpWidget(const example.ListenableBuilderExample()); | ||
|
||
Finder findContainer() => find.descendant(of: find.byType(example.FocusListenerContainer), matching: find.byType(Container)).first; | ||
Finder findChild() => find.descendant(of: findContainer(), matching: find.byType(Column)).first; | ||
bool childHasFocus() => Focus.of(tester.element(findChild())).hasFocus; | ||
Container getContainer() => tester.widget(findContainer()) as Container; | ||
ShapeDecoration getDecoration() => getContainer().decoration! as ShapeDecoration; | ||
OutlinedBorder getBorder() => getDecoration().shape as OutlinedBorder; | ||
|
||
expect(find.text('Company'), findsOneWidget); | ||
expect(find.text('First Name'), findsOneWidget); | ||
expect(find.text('Last Name'), findsOneWidget); | ||
|
||
await tester.tap(find.byType(TextField).first); | ||
await tester.pumpAndSettle(); | ||
expect(childHasFocus(), isFalse); | ||
expect(getBorder().side.width, equals(1)); | ||
expect(getContainer().color, isNull); | ||
expect(getDecoration().color, isNull); | ||
|
||
await tester.tap(find.byType(TextField).at(1)); | ||
await tester.pumpAndSettle(); | ||
expect(childHasFocus(), isTrue); | ||
expect(getBorder().side.width, equals(4)); | ||
expect(getDecoration().color, equals(Colors.blue.shade50)); | ||
|
||
await tester.tap(find.byType(TextField).at(2)); | ||
await tester.pumpAndSettle(); | ||
expect(childHasFocus(), isTrue); | ||
expect(getBorder().side.width, equals(4)); | ||
expect(getDecoration().color, equals(Colors.blue.shade50)); | ||
}); | ||
} |
24 changes: 24 additions & 0 deletions
24
examples/api/test/widgets/transitions/listenable_builder.1_test.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// 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/material.dart'; | ||
import 'package:flutter_api_samples/widgets/transitions/listenable_builder.1.dart' as example; | ||
import 'package:flutter_test/flutter_test.dart'; | ||
|
||
void main() { | ||
testWidgets('Tapping FAB increments counter', (WidgetTester tester) async { | ||
await tester.pumpWidget(const example.ListenableBuilderExample()); | ||
|
||
String getCount() => (tester.widget(find.descendant(of: find.byType(ListenableBuilder), matching: find.byType(Text))) as Text).data!; | ||
|
||
expect(find.text('Current counter value:'), findsOneWidget); | ||
expect(find.text('0'), findsOneWidget); | ||
expect(find.byIcon(Icons.add), findsOneWidget); | ||
expect(getCount(), equals('0')); | ||
|
||
await tester.tap(find.byType(FloatingActionButton).first); | ||
await tester.pumpAndSettle(); | ||
expect(getCount(), equals('1')); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.