diff --git a/README.md b/README.md index 1814d90..b8ce0c3 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Material 3 design that you can use to create your own license page or responsive The package exposes a `MasterDetailsFlow` widget. You can use the widget inside a Scaffold. + ## Usage Create a new MasterDetailsFlow and provide the MasterItems list. Read more in documentation. @@ -49,4 +50,17 @@ Start by creating a new widget and then, inside the widget get the Flow Settings final MasterDetailsFlowSettings? settings = MasterDetailsFlowSettings.of(context); ``` -The MasterDetailsFlow will provide here a method to goBack if it is in page mode, a bool indicating if it is in page mode and app bar settings, but they can be ignored if wanted. Ensure that if you use app bars inside the details page you set `automaticallyImplyLeading: false` and create a way to go back if `settings.selfPage` is `true`. +The MasterDetailsFlow will provide here a method to goBack if it is in page mode, a bool indicating if it is in page mode and app bar settings, but they can be ignored if wanted. Ensure that if you use app bars inside the details page you set `automaticallyImplyLeading: false` and create a way to go back if `settings.selfPage` is `true`. More details can be found in the example app under `example/lib/pages/custom_details_item.dart`. + +## More +In the example app you can find examples of how to create: +* DetailsItem with a centered text +* Custom list +* Demo settings page +* MasterDetailsFlow inside a Future +* Custom DetailsItems +* Custom MasterItem + +Also you should read https://pub.dev/documentation/master_detail_flow/latest/ + +![Screeshot](https://github.com/2-5-perceivers/flutter-master-detail-flow/blob/master/images/s2.png?raw=true)![Screeshot](https://github.com/2-5-perceivers/flutter-master-detail-flow/blob/master/images/s3.png?raw=true)![Screeshot](https://github.com/2-5-perceivers/flutter-master-detail-flow/blob/master/images/s4.png?raw=true)![Screeshot](https://github.com/2-5-perceivers/flutter-master-detail-flow/blob/master/images/s5.png?raw=true) \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 509e550..47de8b4 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,5 @@ +import 'package:example/pages/home.dart'; import 'package:flutter/material.dart'; -import 'package:master_detail_flow/master_detail_flow.dart'; void main() => runApp(const MyApp()); @@ -12,82 +12,18 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: _title, + debugShowCheckedModeBanner: false, theme: ThemeData( useMaterial3: true, brightness: Brightness.light, - colorSchemeSeed: Colors.blue, + colorSchemeSeed: Colors.purple, ), - home: Scaffold( - body: MasterDetailsFlow( - title: const Text(_title), - items: [ - MasterItemHeader( - child: Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 'Cool example', - style: Theme.of(context).textTheme.headlineMedium, - ), - ), - ), - ), - MasterItem( - 'Item one', - detailsBuilder: (_) => const DetailsItem( - title: Text( - 'Item one details title', - ), - ), - ), - MasterItem( - 'Item two', - detailsBuilder: (_) => const DetailsItem( - title: Text( - 'Item two details title', - ), - children: [ - Text('One children'), - ], - ), - ), - MasterItem( - 'Advanced item 3', - detailsBuilder: (_) => DetailsItem( - title: const Text( - 'Using a custom sliver', - ), - slivers: [ - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => Text('This is item $index'), - ), - ), - ], - ), - ), - const MasterItemDivider(), - MasterItem( - 'Item 4', - subtitle: 'Haha', - leading: const Icon(Icons.account_tree_rounded), - detailsBuilder: (_) => DetailsItem( - title: const Text( - 'Using a custom sliver', - ), - actions: [ - IconButton( - onPressed: () {}, - icon: const Icon( - Icons.help, - ), - ), - ], - ), - ), - ], - ), + darkTheme: ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + colorSchemeSeed: Colors.purple, ), + home: const HomePage(), ); } } diff --git a/example/lib/pages/custom_details_item.dart b/example/lib/pages/custom_details_item.dart new file mode 100644 index 0000000..3897d8b --- /dev/null +++ b/example/lib/pages/custom_details_item.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:master_detail_flow/master_detail_flow.dart'; + +/// To create your custom DetailsItem all you need is to get the flow's settings +/// by using `MasterDetailsFlowSettings.of(context)` then you just need to +/// override all the ways to go back, including the app bar ones, so disable +/// implyLeading on all app bars +class CustomDetailsItem extends StatelessWidget { + const CustomDetailsItem({super.key}); + + @override + Widget build(BuildContext context) { + MasterDetailsFlowSettings? settings = MasterDetailsFlowSettings.of(context); + bool selfPage = settings?.selfPage ?? false; + return WillPopScope( + // WillPopScope overrides the system back button so we move back the flow + onWillPop: () async { + if (settings?.selfPage == true) { + settings!.goBack!(); + } + return !(settings?.selfPage ?? false); + }, + child: SizedBox.expand( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(selfPage ? 'This is a page' : 'This is a panel'), + FilledButton.tonalIcon( + onPressed: settings?.goBack, + icon: Icon(Icons.adaptive.arrow_back_rounded), + label: const Text('Go back'), + ), + ], + ), + ), + ), + ); + } +} + +class PageCustom extends StatelessWidget { + const PageCustom({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: MasterDetailsFlow( + title: const Text('Custom DetailsItem'), + items: [ + MasterItem( + 'Custom', + detailsBuilder: (context) => const CustomDetailsItem(), + ), + ], + ), + ); + } +} diff --git a/example/lib/pages/future.dart b/example/lib/pages/future.dart new file mode 100644 index 0000000..1a8f56f --- /dev/null +++ b/example/lib/pages/future.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:master_detail_flow/master_detail_flow.dart'; + +class PageFuture extends StatelessWidget { + const PageFuture({super.key}); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: Future.delayed( + const Duration(seconds: 5), + () => [ + MasterItem( + 'From future 1', + leading: const Icon(Icons.settings_rounded), + detailsBuilder: (context) => + const DetailsItem(title: Text('')), + ), + MasterItem( + 'From future 2', + leading: const Icon(Icons.flutter_dash_rounded), + detailsBuilder: (context) => + const DetailsItem(title: Text('')), + ), + ]), + builder: (context, snapshot) { + return Scaffold( + body: MasterDetailsFlow( + title: const Text('Future'), + nothingSelectedWidget: + snapshot.connectionState == ConnectionState.done + ? null + : Container(), // Used to hide the selection text + items: snapshot.connectionState == ConnectionState.done + ? snapshot.data! + : [ + const _MasterLoading(), + ], + ), + ); + }); + } +} + +/// Custom MasterItem +class _MasterLoading extends StatelessWidget implements MasterItemBase { + const _MasterLoading({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: 200, + child: Center( + child: CircularProgressIndicator(), + ), + ); + } +} diff --git a/example/lib/pages/home.dart b/example/lib/pages/home.dart new file mode 100644 index 0000000..83aa894 --- /dev/null +++ b/example/lib/pages/home.dart @@ -0,0 +1,217 @@ +import 'package:example/pages/custom_details_item.dart'; +import 'package:example/pages/future.dart'; +import 'package:example/pages/settings.dart'; +import 'package:example/widgets/label.dart'; +import 'package:flutter/material.dart'; +import 'package:master_detail_flow/master_detail_flow.dart'; + +class HomePage extends StatelessWidget { + const HomePage({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: MasterDetailsFlow( + title: const Text('Simple flow'), + items: [ + MasterItemHeader( + child: Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'MasterDetailsFlow', + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + ), + ), + MasterItem( + 'Master item 1', + subtitle: 'Tap to learn more about children', + detailsBuilder: (_) => const _HomePageOne(), + ), + MasterItem( + 'Master item 2', + subtitle: 'Tap to learn about slivers', + detailsBuilder: (_) => const _HomePageTwo(), + ), + MasterItem( + 'Master item 3', + detailsBuilder: (_) => const _HomePageThree(), + ), + const MasterItemDivider(), + MasterItem( + 'Master item 4', + subtitle: 'Master items can have icons', + leading: const Icon(Icons.unfold_more_double_outlined), + detailsBuilder: (_) => const _HomePageFour(), + ), + ], + ), + ); + } +} + +class _HomePageFour extends StatelessWidget { + const _HomePageFour(); + + @override + Widget build(BuildContext context) { + return DetailsItem( + title: const Text( + 'Learn more', + ), + children: [ + Padding( + padding: const EdgeInsets.all(32.0), + child: SizedBox( + height: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('See this examples'), + ElevatedButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const PageSettings(), + ), + ); + }, + icon: const Icon(Icons.settings), + label: const Text('A simple settings page'), + ), + ElevatedButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const PageFuture(), + ), + ); + }, + icon: const Icon(Icons.hourglass_empty_rounded), + label: const Text('Use a Future to load'), + ), + ElevatedButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const PageCustom(), + ), + ); + }, + icon: const Icon(Icons.more_vert_outlined), + label: const Text('Custom DetailsItem'), + ), + ], + ), + ), + ), + ], + ); + } +} + +class _HomePageThree extends StatelessWidget { + const _HomePageThree(); + + @override + Widget build(BuildContext context) { + return DetailsItem( + title: const Text( + 'Using a custom list', + ), + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => ListTile( + title: Text('This is item $index'), + ), + ), + ), + ], + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'DetailsItem places all the items inside a CustomScrollView.\nAll the children are put in a SliverListView inside the CustomScrollView, but if you want to use a custom list/grid you can by using a SliverList/SliverGrid inside the slivers field.', + style: Theme.of(context).textTheme.bodySmall, + ), + ), + const LabelText('This example builds and infite amount of children') + ], + ); + } +} + +class _HomePageTwo extends StatelessWidget { + const _HomePageTwo(); + + @override + Widget build(BuildContext context) { + return DetailsItem( + title: const Text( + 'Page two - slivers', + ), + slivers: [ + SliverFillRemaining( + fillOverscroll: false, + hasScrollBody: false, + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'What?', + style: Theme.of(context).textTheme.headlineMedium, + ), + const Text( + 'Using slivers we can create effects(See master item 3) or fill the remaining space to center widgets like this one.', + ), + ], + ), + ), + ), + ) + ], + ); + } +} + +/// This is the details item for the first master item +class _HomePageOne extends StatelessWidget { + const _HomePageOne(); + + @override + Widget build(BuildContext context) { + return DetailsItem( + title: const Text( + 'Page one - children', + ), + children: [ + const LabelText('Children can include everything you want like tiles'), + for (int i = 1; i <= 5; i++) + ListTile( + title: Text('Tile $i'), + subtitle: Text('Tile sub $i'), + leading: Icon( + IconData(0xee34 + i, fontFamily: 'MaterialIcons'), + ), + ), + const LabelText('Info about app?'), + const ListTile( + title: Text('Version'), + subtitle: Text('1.0'), + ), + ], + ); + } +} diff --git a/example/lib/pages/settings.dart b/example/lib/pages/settings.dart new file mode 100644 index 0000000..ec34e95 --- /dev/null +++ b/example/lib/pages/settings.dart @@ -0,0 +1,126 @@ +import 'package:example/widgets/label.dart'; +import 'package:flutter/material.dart'; +import 'package:master_detail_flow/master_detail_flow.dart'; + +class PageSettings extends StatelessWidget { + const PageSettings({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: MasterDetailsFlow( + // As an import page I suggest the large size + masterAppBar: DetailsAppBarSize.large, + nothingSelectedWidget: const Text( + 'Select a settings category from the left panel', + ), + title: const Text('Settings'), + items: [ + MasterItem( + 'Notifications', + leading: const Icon(Icons.settings_rounded), + detailsBuilder: (context) => const _PageSettingsNotifications(), + ), + MasterItem( + 'About app', + leading: const Icon(Icons.flutter_dash_rounded), + detailsBuilder: (context) => const _PageSettingsAbout(), + ), + ], + ), + ); + } +} + +// About +class _PageSettingsAbout extends StatelessWidget { + const _PageSettingsAbout(); + + @override + Widget build(BuildContext context) { + return DetailsItem( + title: const Text('About app'), + children: [ + const SizedBox( + height: 300, + child: Center( + child: FlutterLogo( + size: 200, + ), + ), + ), + // Example date. Use your own + const ListTile( + title: Text('Version'), + subtitle: Text('2.1'), + ), + const ListTile( + title: Text('Made by'), + subtitle: Text('2.5 Perceivers'), + ), + ListTile( + title: const Text('Terms and conditions'), + trailing: Icon(Icons.adaptive.arrow_forward), + ), + ], + ); + } +} + +// Notifications +class _PageSettingsNotifications extends StatefulWidget { + const _PageSettingsNotifications(); + + @override + State<_PageSettingsNotifications> createState() => + _PageSettingsNotificationsState(); +} + +class _PageSettingsNotificationsState + extends State<_PageSettingsNotifications> { + bool push = true, marketing = false, other = true; + + @override + Widget build(BuildContext context) { + return DetailsItem( + title: const Text('Notifications'), + children: [ + const LabelText('App notifications'), + SwitchListTile( + title: const Text('Push notifications'), + value: push, + onChanged: (value) { + setState(() { + push = !push; + }); + }, + ), + SwitchListTile( + title: const Text('Marketing notifications'), + value: marketing, + onChanged: (value) { + setState(() { + marketing = !marketing; + }); + }, + ), + SwitchListTile( + title: const Text('Other random notifications'), + value: other, + onChanged: (value) { + setState(() { + other = !other; + }); + }, + ), + const LabelText('Email notifications'), + const SwitchListTile( + title: Text('Annoying email notifications'), + subtitle: Text('You want these?'), + value: true, + onChanged: null, + ), + ], + ); + } +} diff --git a/example/lib/widgets/label.dart b/example/lib/widgets/label.dart new file mode 100644 index 0000000..884fdc6 --- /dev/null +++ b/example/lib/widgets/label.dart @@ -0,0 +1,25 @@ + +import 'package:flutter/material.dart'; + +class LabelText extends StatelessWidget { + const LabelText(this.data, { + super.key, + }); + + final String data; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Text( + data, + style: Theme.of(context).textTheme.labelLarge!.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + ); + } +} diff --git a/images/s1.png b/images/s1.png index 107596c..eaf2d34 100644 Binary files a/images/s1.png and b/images/s1.png differ diff --git a/images/s2.png b/images/s2.png new file mode 100644 index 0000000..1de7521 Binary files /dev/null and b/images/s2.png differ diff --git a/images/s3.png b/images/s3.png new file mode 100644 index 0000000..8f0f2ed Binary files /dev/null and b/images/s3.png differ diff --git a/images/s4.png b/images/s4.png new file mode 100644 index 0000000..880282c Binary files /dev/null and b/images/s4.png differ diff --git a/images/s5.png b/images/s5.png new file mode 100644 index 0000000..9b3e6c6 Binary files /dev/null and b/images/s5.png differ