From 7031df3a387893779e9ade7af5a70a790a9796ce Mon Sep 17 00:00:00 2001 From: Timm Preetz Date: Wed, 4 Sep 2024 18:57:48 +0200 Subject: [PATCH] Move example into individual files --- example/lib/main.dart | 431 +----------------- .../src/examples/dynamic_pop_navigator.dart | 97 ++++ .../examples/example_selection_navigator.dart | 97 ++++ .../examples/hot_reloadable_navigator.dart | 88 ++++ .../src/examples/list_detail_navigator.dart | 113 +++++ .../examples/overlay_portal_navigator.dart | 56 +++ 6 files changed, 457 insertions(+), 425 deletions(-) create mode 100644 example/lib/src/examples/dynamic_pop_navigator.dart create mode 100644 example/lib/src/examples/example_selection_navigator.dart create mode 100644 example/lib/src/examples/hot_reloadable_navigator.dart create mode 100644 example/lib/src/examples/list_detail_navigator.dart create mode 100644 example/lib/src/examples/overlay_portal_navigator.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index e022949..dc235e0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,5 @@ -import 'dart:async'; - import 'package:fdr/fdr.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:fdr_example/src/examples/example_selection_navigator.dart'; import 'package:flutter/material.dart'; void main() { @@ -11,24 +9,23 @@ void main() { class MyApp extends StatelessWidget { const MyApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'FDR Demo', theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blueAccent), useMaterial3: true, ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), + home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - final String title; + const MyHomePage({ + super.key, + }); @override State createState() => _MyHomePageState(); @@ -42,419 +39,3 @@ class _MyHomePageState extends State { ); } } - -class ExampleSelectionNavigator - extends MappedNavigatableSource { - ExampleSelectionNavigator() : super(initialState: null); - - // Manage state, such that any previous value is always cleaned up - @override - set state(DeclarativeNavigatable? newState) { - final state = this.state; - if (state is DisposableNavigatable) { - state.dispose(); - } - - super.state = newState; - } - - @override - List build() { - final state = this.state; - - return [ - ExampleSelectionPage( - examples: { - 'List Detail': () => ListDetailNavigator(), - 'Dynamic back behavior': () => DynamicPopNavigator(), - 'Stateful Navigator': () => HotReloadableStatefulNavigator(), - 'Overlay Portal': () => OverlayPortalNavigator(), - }, - onExampleSelect: (exampleFactory) => this.state = exampleFactory(), - ).page(onPop: null), - if (state != null) state.poppable(onPop: () => this.state = null) - ]; - } - - @override - void dispose() { - // clean up any potentially created navigators - state = null; - - super.dispose(); - } -} - -class ExampleSelectionPage extends StatelessWidget { - const ExampleSelectionPage({ - super.key, - required this.examples, - required this.onExampleSelect, - }); - - final Map> examples; - - final ValueSetter> onExampleSelect; - - @override - Widget build(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar( - middle: Text('Example selection'), - ), - child: Container( - color: CupertinoColors.systemGroupedBackground, - child: SafeArea( - child: Column( - children: [ - Expanded( - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 20.0), - child: CupertinoFormSection.insetGrouped( - children: [ - for (final entry in examples.entries) - CupertinoListTile.notched( - onTap: () => onExampleSelect(entry.value), - trailing: const CupertinoListTileChevron(), - title: Text(entry.key), - ), - ], - ), - ), - ), - ), - ], - ), - ), - ), - ); - } -} - -class OverlayPortalNavigator extends StatelessNavigator { - @override - List build() { - return [ - const OverlayTogglePage().page(onPop: null), - ]; - } -} - -class OverlayTogglePage extends StatefulWidget { - const OverlayTogglePage({super.key}); - - @override - State createState() => _OverlayTogglePageState(); -} - -class _OverlayTogglePageState extends State { - final controller = OverlayPortalController(); - - @override - Widget build(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar( - middle: Text('Overlay Portal'), - ), - child: SafeArea( - child: OverlayPortal( - controller: controller, - overlayChildBuilder: (context) => Center( - child: GestureDetector( - onTap: controller.toggle, - child: Container( - width: 300, - height: 300, - color: Colors.red.withOpacity(0.8), - ), - ), - ), - child: Center( - child: CupertinoButton.filled( - child: const Text('Show overlay'), - onPressed: () { - controller.toggle(); - }, - ), - ), - ), - ), - ); - } -} - -class DynamicPopNavigator extends MappedNavigatableSource< - ( - bool /* shows child */, - bool /* can pop */, - )> { - DynamicPopNavigator() : super(initialState: (true, false)); - - @override - List build() { - final canPop = state.$2; - - return [ - const Placeholder().page(onPop: null), - if (state.$1) - PopToggle( - value: canPop, - onChange: (v) => state = (true, v), - ).page(onPop: canPop ? () => state = (false, false) : null), - ]; - } -} - -class PopToggle extends StatelessWidget { - const PopToggle({ - super.key, - required this.value, - required this.onChange, - }); - - final bool value; - - final ValueSetter onChange; - - @override - Widget build(BuildContext context) { - if (Theme.of(context).platform == TargetPlatform.android) { - return Scaffold( - appBar: AppBar( - title: const Text('Dynamic pop'), - ), - body: Switch( - value: value, - onChanged: onChange, - ), - ); - } - - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar( - middle: Text('Dynamic Pop'), - ), - child: Container( - color: CupertinoColors.systemGroupedBackground, - child: SafeArea( - child: Column( - children: [ - Expanded( - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 20.0), - child: CupertinoFormSection.insetGrouped( - children: [ - CupertinoListTile.notched( - trailing: CupertinoSwitch( - value: value, - onChanged: onChange, - ), - title: const Text('Can pop'), - ), - ], - ), - ), - ), - ), - ], - ), - ), - ), - ); - } -} - -class ListDetailNavigator extends MappedNavigatableSource { - ListDetailNavigator() : super(initialState: null); - - @override - List build() { - final state = this.state; - - return [ - NumberSelectionPage( - onNumberSelect: (number) => this.state = number, - ).page(onPop: null), - if (state != null) - if (state == 7) - LuckyNumberSevenPage( - onTap: () => this.state = null, - ).popup(onPop: () => this.state = null) - else - NumberDetailPage(number: state).page(onPop: () => this.state = null), - ]; - } -} - -class NumberSelectionPage extends StatelessWidget { - const NumberSelectionPage({ - super.key, - required this.onNumberSelect, - }); - - final ValueSetter onNumberSelect; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Pick a number'), - ), - body: ListView.builder( - itemCount: 1000, - itemBuilder: (context, index) { - final n = index + 1; - - return CupertinoListTile( - title: Text('$n'), - trailing: const CupertinoListTileChevron(), - onTap: () => onNumberSelect(n), - ); - }, - ), - ); - } -} - -class NumberDetailPage extends StatelessWidget { - const NumberDetailPage({ - super.key, - required this.number, - }); - - final int number; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Your choice'), - ), - body: Center( - child: Text( - '$number', - style: const TextStyle(fontSize: 90), - ), - ), - ); - } -} - -class LuckyNumberSevenPage extends StatelessWidget { - const LuckyNumberSevenPage({ - super.key, - required this.onTap, - }); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar( - middle: Text('Lucky you!'), - automaticallyImplyLeading: false, - ), - child: SafeArea( - child: SizedBox( - width: 300, - child: GestureDetector( - onTap: onTap, - child: const Text( - '🥳', - style: TextStyle(fontSize: 90), - ), - ), - ), - ), - ); - } -} - -class HotReloadableStatefulNavigator extends MappedNavigatableSource { - HotReloadableStatefulNavigator() : super(initialState: null); - - @override - List build() { - return [ - StatefulNavigatorDemo(initialCount: 100), - ]; - } -} - -class StatefulNavigatorDemo extends StatefulNavigator { - StatefulNavigatorDemo({ - required this.initialCount, - }); - - final int initialCount; - - @override - StatefulNavigatorState createState() => - _StatefulNavigatorDemoState(); -} - -class _StatefulNavigatorDemoState - extends StatefulNavigatorState { - late final Timer timer; - - late int ticks; - - @override - void initState() { - super.initState(); - - ticks = navigator.initialCount; - - timer = Timer.periodic(const Duration(seconds: 1), (_) { - setState(() { - ticks++; - }); - }); - } - - @override - void didUpdateNavigator(covariant StatefulNavigatorDemo oldNavigator) { - super.didUpdateNavigator(oldNavigator); - - if (navigator.initialCount != oldNavigator.initialCount) { - ticks = navigator.initialCount; - // for the best behavior, we would also have to reset the timer here - } - } - - @override - List build() { - return [ - CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar( - middle: Text('Ticks'), // TODO(tp): Check hot reload on change - ), - child: SafeArea( - child: Scaffold( - body: Center( - child: Text( - '$ticks', - style: const TextStyle( - fontSize: 90, - fontFeatures: [FontFeature.tabularFigures()], - ), - ), - ), - ), - ), - ).page(onPop: null), - ]; - } - - @override - void dispose() { - timer.cancel(); - - super.dispose(); - } -} diff --git a/example/lib/src/examples/dynamic_pop_navigator.dart b/example/lib/src/examples/dynamic_pop_navigator.dart new file mode 100644 index 0000000..893b251 --- /dev/null +++ b/example/lib/src/examples/dynamic_pop_navigator.dart @@ -0,0 +1,97 @@ +import 'package:fdr/fdr.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class DynamicPopNavigator extends MappedNavigatableSource< + ( + bool /* shows child */, + bool /* can pop */, + )> { + DynamicPopNavigator() : super(initialState: (true, false)); + + @override + List build() { + final canPop = state.$2; + + return [ + Scaffold( + appBar: AppBar( + title: const Text('Dynamic pop parent'), + ), + body: Center( + child: FilledButton( + onPressed: () => state = (true, false), + child: const Text('Open child page'), + ), + ), + ).page(onPop: null), + if (state.$1) + PopToggle( + value: canPop, + onChange: (v) => state = (true, v), + ).page(onPop: canPop ? () => state = (false, false) : null), + ]; + } +} + +class PopToggle extends StatelessWidget { + @visibleForTesting + const PopToggle({ + super.key, + required this.value, + required this.onChange, + }); + + final bool value; + + final ValueSetter onChange; + + @override + Widget build(BuildContext context) { + if (Theme.of(context).platform == TargetPlatform.android) { + return Scaffold( + appBar: AppBar( + title: const Text('Dynamic pop'), + ), + body: Switch( + value: value, + onChanged: onChange, + ), + ); + } + + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar( + middle: Text('Dynamic Pop'), + ), + child: Container( + color: CupertinoColors.systemGroupedBackground, + child: SafeArea( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20.0), + child: CupertinoFormSection.insetGrouped( + children: [ + CupertinoListTile.notched( + trailing: CupertinoSwitch( + value: value, + onChanged: onChange, + ), + title: const Text('Can pop'), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/example/lib/src/examples/example_selection_navigator.dart b/example/lib/src/examples/example_selection_navigator.dart new file mode 100644 index 0000000..28fe05b --- /dev/null +++ b/example/lib/src/examples/example_selection_navigator.dart @@ -0,0 +1,97 @@ +import 'package:fdr/fdr.dart'; +import 'package:fdr_example/src/examples/dynamic_pop_navigator.dart'; +import 'package:fdr_example/src/examples/hot_reloadable_navigator.dart'; +import 'package:fdr_example/src/examples/list_detail_navigator.dart'; +import 'package:fdr_example/src/examples/overlay_portal_navigator.dart'; +import 'package:flutter/cupertino.dart'; + +class ExampleSelectionNavigator + extends MappedNavigatableSource { + ExampleSelectionNavigator() : super(initialState: null); + + // Manage state, such that any previous value is always cleaned up + @override + set state(DeclarativeNavigatable? newState) { + final state = this.state; + if (state is DisposableNavigatable) { + state.dispose(); + } + + super.state = newState; + } + + @override + List build() { + final state = this.state; + + return [ + ExampleSelectionPage( + examples: { + 'List Detail': () => ListDetailNavigator(), + 'Dynamic back behavior': () => DynamicPopNavigator(), + 'Stateful Navigator w/ hot reload': () => HotReloadableNavigator(), + 'Overlay Portal': () => OverlayPortalNavigator(), + }, + onExampleSelect: (exampleFactory) => this.state = exampleFactory(), + ).page(onPop: null), + if (state != null) state.poppable(onPop: () => this.state = null) + ]; + } + + @override + void dispose() { + // clean up any potentially created navigators + state = null; + + super.dispose(); + } +} + +class ExampleSelectionPage extends StatelessWidget { + @visibleForTesting + const ExampleSelectionPage({ + super.key, + required this.examples, + required this.onExampleSelect, + }); + + final Map> examples; + + final ValueSetter> onExampleSelect; + + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar( + middle: Text('Example selection'), + ), + child: Container( + color: CupertinoColors.systemGroupedBackground, + child: SafeArea( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20.0), + child: CupertinoFormSection.insetGrouped( + children: [ + for (final entry in examples.entries) + CupertinoListTile.notched( + onTap: () => onExampleSelect(entry.value), + trailing: const CupertinoListTileChevron(), + title: Text(entry.key), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/example/lib/src/examples/hot_reloadable_navigator.dart b/example/lib/src/examples/hot_reloadable_navigator.dart new file mode 100644 index 0000000..90f4424 --- /dev/null +++ b/example/lib/src/examples/hot_reloadable_navigator.dart @@ -0,0 +1,88 @@ +import 'dart:async'; + +import 'package:fdr/fdr.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class HotReloadableNavigator extends StatelessNavigator { + @override + List build() { + return [ + StatefulNavigatorDemo(initialCount: 100), + ]; + } +} + +class StatefulNavigatorDemo extends StatefulNavigator { + @visibleForTesting + StatefulNavigatorDemo({ + required this.initialCount, + }); + + final int initialCount; + + @override + StatefulNavigatorState createState() => + _StatefulNavigatorDemoState(); +} + +class _StatefulNavigatorDemoState + extends StatefulNavigatorState { + late final Timer timer; + + late int ticks; + + @override + void initState() { + super.initState(); + + ticks = navigator.initialCount; + + timer = Timer.periodic(const Duration(seconds: 1), (_) { + setState(() { + ticks++; + }); + }); + } + + @override + void didUpdateNavigator(covariant StatefulNavigatorDemo oldNavigator) { + super.didUpdateNavigator(oldNavigator); + + if (navigator.initialCount != oldNavigator.initialCount) { + ticks = navigator.initialCount; + // for the best behavior, we would also have to reset the timer here + } + } + + @override + List build() { + return [ + CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar( + middle: Text('Ticks'), // TODO(tp): Check hot reload on change + ), + child: SafeArea( + child: Scaffold( + body: Center( + child: Text( + '$ticks', + style: const TextStyle( + fontSize: 90, + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ), + ), + ), + ).page(onPop: null), + ]; + } + + @override + void dispose() { + timer.cancel(); + + super.dispose(); + } +} diff --git a/example/lib/src/examples/list_detail_navigator.dart b/example/lib/src/examples/list_detail_navigator.dart new file mode 100644 index 0000000..3087482 --- /dev/null +++ b/example/lib/src/examples/list_detail_navigator.dart @@ -0,0 +1,113 @@ +import 'package:fdr/fdr.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class ListDetailNavigator extends MappedNavigatableSource { + ListDetailNavigator() : super(initialState: null); + + @override + List build() { + final state = this.state; + + return [ + NumberSelectionPage( + onNumberSelect: (number) => this.state = number, + ).page(onPop: null), + if (state != null) + if (state == 7) + LuckyNumberSevenPage( + onTap: () => this.state = null, + ).popup(onPop: () => this.state = null) + else + NumberDetailPage(number: state).page(onPop: () => this.state = null), + ]; + } +} + +class NumberSelectionPage extends StatelessWidget { + @visibleForTesting + const NumberSelectionPage({ + super.key, + required this.onNumberSelect, + }); + + final ValueSetter onNumberSelect; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Pick a number'), + ), + body: ListView.builder( + itemCount: 1000, + itemBuilder: (context, index) { + final n = index + 1; + + return CupertinoListTile( + title: Text('$n'), + trailing: const CupertinoListTileChevron(), + onTap: () => onNumberSelect(n), + ); + }, + ), + ); + } +} + +class NumberDetailPage extends StatelessWidget { + @visibleForTesting + const NumberDetailPage({ + super.key, + required this.number, + }); + + final int number; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Your choice'), + ), + body: Center( + child: Text( + '$number', + style: const TextStyle(fontSize: 90), + ), + ), + ); + } +} + +class LuckyNumberSevenPage extends StatelessWidget { + @visibleForTesting + const LuckyNumberSevenPage({ + super.key, + required this.onTap, + }); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar( + middle: Text('Lucky you!'), + automaticallyImplyLeading: false, + ), + child: SafeArea( + child: SizedBox( + width: 300, + child: GestureDetector( + onTap: onTap, + child: const Text( + '🥳', + style: TextStyle(fontSize: 90), + ), + ), + ), + ), + ); + } +} diff --git a/example/lib/src/examples/overlay_portal_navigator.dart b/example/lib/src/examples/overlay_portal_navigator.dart new file mode 100644 index 0000000..ae2bec5 --- /dev/null +++ b/example/lib/src/examples/overlay_portal_navigator.dart @@ -0,0 +1,56 @@ +import 'package:fdr/fdr.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class OverlayPortalNavigator extends StatelessNavigator { + @override + List build() { + return [ + const OverlayTogglePage().page(onPop: null), + ]; + } +} + +class OverlayTogglePage extends StatefulWidget { + @visibleForTesting + const OverlayTogglePage({super.key}); + + @override + State createState() => _OverlayTogglePageState(); +} + +class _OverlayTogglePageState extends State { + final controller = OverlayPortalController(); + + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar( + middle: Text('Overlay Portal'), + ), + child: SafeArea( + child: OverlayPortal( + controller: controller, + overlayChildBuilder: (context) => Center( + child: GestureDetector( + onTap: controller.toggle, + child: Container( + width: 300, + height: 300, + color: Colors.red.withOpacity(0.8), + ), + ), + ), + child: Center( + child: CupertinoButton.filled( + child: const Text('Show overlay'), + onPressed: () { + controller.toggle(); + }, + ), + ), + ), + ), + ); + } +}