diff --git a/packages/cupertino_ui/CHANGELOG.md b/packages/cupertino_ui/CHANGELOG.md index 23017e9f58e6..9486d626a0f0 100644 --- a/packages/cupertino_ui/CHANGELOG.md +++ b/packages/cupertino_ui/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. +* Adds an example application demonstrating the package, shown on the pub.dev "Example" tab. ## 0.0.1 diff --git a/packages/cupertino_ui/example/README.md b/packages/cupertino_ui/example/README.md index c80ea394e704..db80fca44a96 100644 --- a/packages/cupertino_ui/example/README.md +++ b/packages/cupertino_ui/example/README.md @@ -1,6 +1,12 @@ # cupertino_ui API Example Code -This directory contains the example code that is referenced in the documentation -in cupertino_ui's source code. + +[`lib/main.dart`](lib/main.dart) is a small showcase app that demonstrates +`cupertino_ui`'s theming and a selection of commonly used components. It is the +example shown on the package's [pub.dev](https://pub.dev) **Example** tab, and +can be run from this directory with `flutter run`. + +The rest of this directory contains the API documentation code samples that are +referenced from the documentation in cupertino_ui's source code. These examples were originally located [in flutter/flutter](https://github.com/flutter/flutter/tree/master/examples/api) diff --git a/packages/cupertino_ui/example/lib/main.dart b/packages/cupertino_ui/example/lib/main.dart new file mode 100644 index 000000000000..d12707396f87 --- /dev/null +++ b/packages/cupertino_ui/example/lib/main.dart @@ -0,0 +1,201 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cupertino_ui/cupertino_ui.dart'; + +void main() { + runApp(const CupertinoExampleApp()); +} + +/// A small showcase app for the cupertino_ui package. +class CupertinoExampleApp extends StatefulWidget { + const CupertinoExampleApp({super.key}); + + @override + State createState() => _CupertinoExampleAppState(); +} + +class _CupertinoExampleAppState extends State { + // A null brightness follows the system setting. + Brightness? _brightness; + + void _setBrightness(Brightness? brightness) { + setState(() { + _brightness = brightness; + }); + } + + @override + Widget build(BuildContext context) { + return CupertinoApp( + title: 'cupertino_ui example', + debugShowCheckedModeBanner: false, + theme: CupertinoThemeData(brightness: _brightness), + home: _HomeScreen( + brightness: _brightness, + onBrightnessChanged: _setBrightness, + ), + ); + } +} + +class _HomeScreen extends StatefulWidget { + const _HomeScreen({ + required this.brightness, + required this.onBrightnessChanged, + }); + + final Brightness? brightness; + final ValueChanged onBrightnessChanged; + + @override + State<_HomeScreen> createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State<_HomeScreen> { + bool _notificationsEnabled = true; + double _volume = 0.5; + final TextEditingController _nameController = TextEditingController(); + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + Future _showInfoDialog() { + return showCupertinoDialog( + context: context, + builder: (BuildContext context) { + return CupertinoAlertDialog( + title: const Text('cupertino_ui'), + content: const Text( + 'The official Cupertino widget library for Flutter, as a ' + 'standalone package.', + ), + actions: [ + CupertinoDialogAction( + isDefaultAction: true, + onPressed: () => Navigator.pop(context), + child: const Text('OK'), + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(middle: Text('cupertino_ui')), + child: SafeArea( + child: ListView( + children: [ + CupertinoListSection.insetGrouped( + header: const Text('Appearance'), + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 6, + ), + child: SizedBox( + width: double.infinity, + child: CupertinoSlidingSegmentedControl( + groupValue: _brightnessToSegment(widget.brightness), + onValueChanged: (int? value) { + if (value != null) { + widget.onBrightnessChanged( + _segmentToBrightness(value), + ); + } + }, + children: const { + 0: _SegmentLabel('Auto'), + 1: _SegmentLabel('Light'), + 2: _SegmentLabel('Dark'), + }, + ), + ), + ), + ], + ), + CupertinoListSection.insetGrouped( + header: const Text('Controls'), + hasLeading: false, + children: [ + CupertinoListTile( + title: const Text('Notifications'), + trailing: CupertinoSwitch( + value: _notificationsEnabled, + onChanged: (bool value) { + setState(() => _notificationsEnabled = value); + }, + ), + ), + CupertinoListTile( + title: const Text('Volume'), + subtitle: CupertinoSlider( + value: _volume, + onChanged: (double value) { + setState(() => _volume = value); + }, + ), + ), + ], + ), + CupertinoListSection.insetGrouped( + header: const Text('Profile'), + children: [ + CupertinoTextField.borderless( + controller: _nameController, + placeholder: 'Name', + padding: const EdgeInsets.all(16), + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: CupertinoButton.filled( + onPressed: _showInfoDialog, + child: const Text('About this package'), + ), + ), + ], + ), + ), + ); + } + + static int _brightnessToSegment(Brightness? brightness) { + return switch (brightness) { + null => 0, + Brightness.light => 1, + Brightness.dark => 2, + }; + } + + static Brightness? _segmentToBrightness(int segment) { + return switch (segment) { + 1 => Brightness.light, + 2 => Brightness.dark, + _ => null, + }; + } +} + +class _SegmentLabel extends StatelessWidget { + const _SegmentLabel(this.text); + + final String text; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text(text), + ); + } +} diff --git a/packages/cupertino_ui/example/test/main_test.dart b/packages/cupertino_ui/example/test/main_test.dart new file mode 100644 index 000000000000..c7ddddd9cadb --- /dev/null +++ b/packages/cupertino_ui/example/test/main_test.dart @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cupertino_ui/cupertino_ui.dart'; +import 'package:cupertino_ui_examples/main.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('renders the showcase components', (WidgetTester tester) async { + await tester.pumpWidget(const example.CupertinoExampleApp()); + + expect(find.byType(CupertinoListSection), findsNWidgets(3)); + expect(find.byType(CupertinoSlidingSegmentedControl), findsOneWidget); + expect(find.byType(CupertinoSwitch), findsOneWidget); + expect(find.byType(CupertinoSlider), findsOneWidget); + expect(find.byType(CupertinoTextField), findsOneWidget); + expect(tester.takeException(), isNull); + }); + + testWidgets('changes brightness via the segmented control', ( + WidgetTester tester, + ) async { + await tester.pumpWidget(const example.CupertinoExampleApp()); + + CupertinoApp app = tester.widget(find.byType(CupertinoApp)); + expect(app.theme?.brightness, isNull); + + await tester.tap(find.text('Dark')); + await tester.pumpAndSettle(); + + app = tester.widget(find.byType(CupertinoApp)); + expect(app.theme?.brightness, Brightness.dark); + }); + + testWidgets('opens and dismisses the info dialog', ( + WidgetTester tester, + ) async { + await tester.pumpWidget(const example.CupertinoExampleApp()); + + await tester.tap(find.text('About this package')); + await tester.pumpAndSettle(); + expect(find.byType(CupertinoAlertDialog), findsOneWidget); + + await tester.tap(find.text('OK')); + await tester.pumpAndSettle(); + expect(find.byType(CupertinoAlertDialog), findsNothing); + }); + + testWidgets('toggles the notifications switch', (WidgetTester tester) async { + await tester.pumpWidget(const example.CupertinoExampleApp()); + + CupertinoSwitch toggle = tester.widget(find.byType(CupertinoSwitch)); + expect(toggle.value, isTrue); + + await tester.tap(find.byType(CupertinoSwitch)); + await tester.pumpAndSettle(); + + toggle = tester.widget(find.byType(CupertinoSwitch)); + expect(toggle.value, isFalse); + }); +} diff --git a/packages/material_ui/CHANGELOG.md b/packages/material_ui/CHANGELOG.md index 67d0411f3245..a408963803e0 100644 --- a/packages/material_ui/CHANGELOG.md +++ b/packages/material_ui/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. +* Adds an example application demonstrating the package, shown on the pub.dev "Example" tab. ## 0.0.1 diff --git a/packages/material_ui/example/README.md b/packages/material_ui/example/README.md index ec71ef9ac04d..d851f48d4840 100644 --- a/packages/material_ui/example/README.md +++ b/packages/material_ui/example/README.md @@ -1,6 +1,12 @@ # material_ui API Example Code -This directory contains the example code that is referenced in the documentation -in material_ui's source code. + +[`lib/main.dart`](lib/main.dart) is a small showcase app that demonstrates +`material_ui`'s theming and a selection of commonly used components. It is the +example shown on the package's [pub.dev](https://pub.dev) **Example** tab, and +can be run from this directory with `flutter run`. + +The rest of this directory contains the API documentation code samples that are +referenced from the documentation in material_ui's source code. These examples were originally located [in flutter/flutter](https://github.com/flutter/flutter/tree/master/examples/api) diff --git a/packages/material_ui/example/lib/main.dart b/packages/material_ui/example/lib/main.dart new file mode 100644 index 000000000000..6561bf644b35 --- /dev/null +++ b/packages/material_ui/example/lib/main.dart @@ -0,0 +1,180 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:material_ui/material_ui.dart'; + +void main() { + runApp(const MaterialExampleApp()); +} + +/// A small showcase app for the material_ui package. +class MaterialExampleApp extends StatefulWidget { + const MaterialExampleApp({super.key}); + + @override + State createState() => _MaterialExampleAppState(); +} + +class _MaterialExampleAppState extends State { + ThemeMode _themeMode = ThemeMode.system; + + void _setThemeMode(ThemeMode mode) { + setState(() { + _themeMode = mode; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'material_ui example', + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo), + ), + darkTheme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.indigo, + brightness: Brightness.dark, + ), + ), + themeMode: _themeMode, + home: _HomeScreen( + themeMode: _themeMode, + onThemeModeChanged: _setThemeMode, + ), + ); + } +} + +class _HomeScreen extends StatefulWidget { + const _HomeScreen({ + required this.themeMode, + required this.onThemeModeChanged, + }); + + final ThemeMode themeMode; + final ValueChanged onThemeModeChanged; + + @override + State<_HomeScreen> createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State<_HomeScreen> { + int _counter = 0; + bool _notificationsEnabled = true; + double _volume = 0.5; + final Set _selectedTopics = {'Flutter'}; + + static const List _topics = ['Flutter', 'Material', 'Dart']; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final TextStyle? sectionStyle = theme.textTheme.titleMedium; + + return Scaffold( + appBar: AppBar(title: const Text('material_ui')), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + Text('Appearance', style: sectionStyle), + const SizedBox(height: 8), + SegmentedButton( + segments: const >[ + ButtonSegment( + value: ThemeMode.system, + label: Text('System'), + icon: Icon(Icons.brightness_auto), + ), + ButtonSegment( + value: ThemeMode.light, + label: Text('Light'), + icon: Icon(Icons.light_mode), + ), + ButtonSegment( + value: ThemeMode.dark, + label: Text('Dark'), + icon: Icon(Icons.dark_mode), + ), + ], + selected: {widget.themeMode}, + onSelectionChanged: (Set selection) { + widget.onThemeModeChanged(selection.first); + }, + ), + const SizedBox(height: 24), + Text('Buttons', style: sectionStyle), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + FilledButton(onPressed: () {}, child: const Text('Filled')), + ElevatedButton(onPressed: () {}, child: const Text('Elevated')), + OutlinedButton(onPressed: () {}, child: const Text('Outlined')), + TextButton(onPressed: () {}, child: const Text('Text')), + ], + ), + const SizedBox(height: 24), + Text('Topics', style: sectionStyle), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: [ + for (final String topic in _topics) + FilterChip( + label: Text(topic), + selected: _selectedTopics.contains(topic), + onSelected: (bool selected) { + setState(() { + if (selected) { + _selectedTopics.add(topic); + } else { + _selectedTopics.remove(topic); + } + }); + }, + ), + ], + ), + const SizedBox(height: 24), + Card( + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + SwitchListTile( + title: const Text('Enable notifications'), + value: _notificationsEnabled, + onChanged: (bool value) { + setState(() => _notificationsEnabled = value); + }, + ), + const Divider(height: 1), + ListTile( + title: const Text('Volume'), + subtitle: Slider( + value: _volume, + onChanged: (double value) { + setState(() => _volume = value); + }, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + Center( + child: Text('Button tapped $_counter times', style: sectionStyle), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () => setState(() => _counter++), + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/packages/material_ui/example/test/main_test.dart b/packages/material_ui/example/test/main_test.dart new file mode 100644 index 000000000000..77d58850a9b4 --- /dev/null +++ b/packages/material_ui/example/test/main_test.dart @@ -0,0 +1,75 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:material_ui/material_ui.dart'; +import 'package:material_ui_examples/main.dart' as example; + +void main() { + testWidgets('renders the showcase components', (WidgetTester tester) async { + await tester.pumpWidget(const example.MaterialExampleApp()); + + expect(find.byType(SegmentedButton), findsOneWidget); + expect(find.byType(FilledButton), findsOneWidget); + expect(find.byType(FilterChip), findsNWidgets(3)); + expect(find.byType(Card), findsOneWidget); + expect(find.byType(Slider), findsOneWidget); + expect(find.byType(FloatingActionButton), findsOneWidget); + expect(tester.takeException(), isNull); + }); + + testWidgets('increments the counter when the FAB is tapped', ( + WidgetTester tester, + ) async { + await tester.pumpWidget(const example.MaterialExampleApp()); + expect(find.text('Button tapped 0 times'), findsOneWidget); + + await tester.tap(find.byType(FloatingActionButton)); + await tester.pump(); + + expect(find.text('Button tapped 1 times'), findsOneWidget); + }); + + testWidgets('changes theme mode via the segmented button', ( + WidgetTester tester, + ) async { + await tester.pumpWidget(const example.MaterialExampleApp()); + + MaterialApp app = tester.widget(find.byType(MaterialApp)); + expect(app.themeMode, ThemeMode.system); + + await tester.tap(find.text('Dark')); + await tester.pumpAndSettle(); + + app = tester.widget(find.byType(MaterialApp)); + expect(app.themeMode, ThemeMode.dark); + }); + + testWidgets('toggles the notifications switch', (WidgetTester tester) async { + await tester.pumpWidget(const example.MaterialExampleApp()); + + SwitchListTile tile = tester.widget(find.byType(SwitchListTile)); + expect(tile.value, isTrue); + + await tester.tap(find.byType(SwitchListTile)); + await tester.pumpAndSettle(); + + tile = tester.widget(find.byType(SwitchListTile)); + expect(tile.value, isFalse); + }); + + testWidgets('toggles a filter chip selection', (WidgetTester tester) async { + await tester.pumpWidget(const example.MaterialExampleApp()); + + // The 'Flutter' topic starts selected; tapping it clears the selection. + FilterChip chip = tester.widget(find.widgetWithText(FilterChip, 'Flutter')); + expect(chip.selected, isTrue); + + await tester.tap(find.text('Flutter')); + await tester.pumpAndSettle(); + + chip = tester.widget(find.widgetWithText(FilterChip, 'Flutter')); + expect(chip.selected, isFalse); + }); +}