wolt_modal_sheet 0.4.1 wolt_modal_sheet: ^0.4.1 copied to clipboard
This package provides a responsive modal with multiple pages, motion animation for page transitions, and scrollable content within each page.
WoltModalSheet #
WoltModalSheet is designed to revolutionize the use of Flutter modal sheets. Built with Wolt-grade design quality and used extensively in Wolt products, this UI component offers a visually appealing and user-friendly modal sheet with multiple pages, motion animation for page transitions, and scrollable content within each page.
Features #
Multi-Page Layout #
Traverse through numerous pages within a single sheet.
Scrollable Content #
Greater flexibility with scrollable content per page, accommodating large content effortlessly.
Responsive Design #
The modal sheet adjusts to fit all screen sizes, appearing as a dialog on larger screens and as a bottom sheet on smaller screens, guided by user-specified conditions.
Motion Animation #
Engage users with dynamic motion animation for page transitions and scrolling.
Pagination | Scrolling |
---|---|
Imperative and Declarative Navigation #
The library showcases examples of both imperative and declarative navigation patterns to display modal sheet on screen.
Dynamic Pagination #
User input can dynamically shape the modal sheet's page list.
State Management Integration #
Pages in the Wolt Modal Sheet offer a customizable look and the page components
are provided with an instance of WoltModalSheetPage class. The API provides a
way
to manage the state among the page components to be used with popular libraries
such as Bloc and Provider
Understanding the page elements #
Each element within the WoltModalSheet has a role to play, offering context, navigational assistance, and explicit action prompts to the user. By understanding these elements and their roles, you can fully harness the power of WoltModalSheet and create an intuitive and engaging user experience.
The structure is organized across layers on the z-axis:
By employing these various layers, you can create an interactive and visually
appealing interface that resonates with users. Each layer contributes to the
overall coherence of the page, serving a specific purpose and enhancing the
overall user experience.
Navigation bar widgets #
The navigation bar has a transparent background, and resides at the top of
the sheet, situated directly above the top bar on the z-axis. It includes
two specific widgets: the leading and the trailing. The leading widget
usually functions as the back button, enabling users to navigate to the
previous page. The trailing widget often serves as the close button, utilized to
close the modal sheet. The middle area is reserved and left empty for the
visibility of the top bar title.
The navigation bar widgets provide clear and intuitive navigational control,
differentiating themselves from the top bar by focusing specifically on
directional navigation within the interface.
Top bar and top bar title #
The top bar layer sits above the main content layer and below the navigation
bar layer in z axis. It helps users grasping the context by displaying an
optional title. In scenarios where the sheet is filled with content
requiring scrolling, the top bar becomes visible as the user scrolls, and
replaces the page title. At this point, the top bar adopts a 'sticky'
position at the top, guaranteeing consistent visibility.
The top bar widget has a flexible design. When hasTopBarLayer
is set to
false, the top bar and the top bar title will not be shown. If
isTopBarLayerAlwaysVisible
set to true, the top bar will be always visible
regardless of the scroll position.
A custom top bar widget can be provided using the topBar
field. In this
case, the topBarTitle
field will be ignored, and will not be displayed.
The navigation bar widgets overlay above the top bar, and when the default
top bar widget is used in the page, the top bar title is symmetrically
framed between the leading and trailing navigation bar widgets.
Sticky action bar (SAB) #
The Sticky Action Bar (SAB) guides the user towards the next step. Anchored to the bottom of the view, the SAB elevates above the content with an optional gentle gradient. This position guarantees that the action remains visible, subtly hinting to the user that there is more content to be explored below the fold by scrolling.
Hero image #
An optional Hero Image can be positioned at the top of the main content. This element immediately grabs the user's attention, effectively conveying the primary theme or message of the content.
Page Title #
An optional page title above the main content provides users with a quick understanding of what to expect from the page. As the user scrolls, this title becomes hidden, at which point the top bar title continues to serve this context-providing purpose.
Main content #
The main content delivers information according to the user need. It can be scrollable to handle larger content. The content is built lazily to improve the performance.
Here is an example that shows all the modal sheet elements in use:
Usage of WoltModalSheet Pages #
The WoltModalSheet library provides three primary classes for constructing
modal sheet pages: WoltModalSheetPage
, SliverWoltModalSheetPage
, and
NonScrollingWoltModalSheetPage
. Understanding the use cases and
functionalities of these classes is key to creating performant,
easy-to-construct modal sheets.
SliverWoltModalSheetPage #
SliverWoltModalSheetPage
is designed for complex and dynamic content
layouts within a modal sheet. It leverages the power of Flutter's Sliver
widgets to provide flexible and efficient scrolling behaviors.
SliverWoltModalSheetPage(
mainContentSlivers: [
SliverList(
delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
// Your list items
}),
),
// Other sliver widgets...
],
// Additional page elements like pageTitle, topBarTitle, etc.
)
WoltModalSheetPage #
WoltModalSheetPage provides a simpler alternative for pages that primarily consist of a single widget or a straightforward layout. It automatically wraps the child widget in a SliverToBoxAdapter, making it suitable for use in sliver-based scrollable layouts.
Key Features:
- Simplicity: Ideal for single-widget content or basic layouts.
- No Sliver Overhead: Automatically handles the wrapping of non-sliver widgets into slivers.
- Ease of Use: Simplifies the process of creating modal sheet pages without needing to deal with slivers directly.
WoltModalSheetPage(
child: MyCustomContentWidget(),
pageTitle: Text('My Page Title'),
// Other properties...
)
NonScrollingWoltModalSheetPage #
NonScrollingWoltModalSheetPage
is designed to display content which is
flexible in height but unlikely to require scrolling. This class is ideal
for content that adapts to the available vertical space within the modal
sheet's maximum height, but is unlikely to exceed that height and require
scrolling.
Key Features:
- Adaptability: Designed for content with flexible height but fixed or intrinsic dimensions.
- Flex Layout: Can utilize the Flex layout model of a Column for effective space management.
- Non-Scrolling: Best for content that fits within the modal sheet's maximum height without needing scrolling.
Warning: If there is a risk that the content's height might exceed the modal sheet's maximum height, leading to overflow, it is recommended to use SliverWoltModalSheetPage or WoltModalSheetPage instead. These classes provide scrolling capabilities to handle larger content effectively using slivers.
NonScrollingWoltModalSheetPage(
child: MyFlexibleHeightWidget(),
// Additional properties...
)
This class extends SliverWoltModalSheetPage, offering a streamlined approach to handle non-scrolling content within a modal sheet.
Choosing between the three #
When deciding which class to use for your modal sheet, consider the following guidelines:
-
WoltModalSheetPage: Choose this for simpler content layouts, especially when working with a single widget. It's best suited for straightforward layouts that don't require the complexities of Slivers.
-
SliverWoltModalSheetPage: Opt for this class when your modal sheet requires complex scrolling behaviors or needs to display a long list of items. It's ideal for dynamic content layouts that benefit from the advanced capabilities of Flutter's Sliver widgets.
-
NonScrollingWoltModalSheetPage: This class is best when your content is flexible in height but unlikely to require scrolling. It’s perfect for modal sheets where the content fits within the modal's maximum height without the need for scrollable behavior. Use this for content with fixed or intrinsic dimensions that need to adapt to available vertical space.
Migration from 0.1.x to 0.2.0 #
This section provides detailed guidance on the breaking changes introduced in
version 0.2.0, particularly focusing on the usage of the WoltModalSheetPage
class.
Changes Overview
- The previous constructors
WoltModalSheetPage.withSingleChild
andWoltModalSheetPage.withCustomSliverList
have been removed in this update. - We have introduced a new class,
SliverWoltModalSheetPage
, which now serves as the base class for pages. This new class is intended to replace theWoltModalSheetPage.withCustomSliverList
constructor. - The
WoltModalSheetPage
class has been updated to extend fromSliverWoltModalSheetPage
. This substitutes theWoltModalSheetPage. withSingleChild
constructor. - The
mainContentSlivers
property is now added toSliverWoltModalSheetPage
to replace thesliverList
property ofWoltModalSheetPage.withCustomSliverList
. This allows using list of sliver widgets instead of a single sliver list in sliver pages.
Migration Steps
- If your previous implementation used
WoltModalSheetPage.withSingleChild
, you can now directly transition to usingWoltModalSheetPage
.
// Before
WoltModalSheetPage.withSingleChild(child: MyWidget());
// After
WoltModalSheetPage(child: MyWidget());
- If you were utilizing
WoltModalSheetPage.withCustomSliverList
for complex, sliver-based content, switch toSliverWoltModalSheetPage
. Utilize themainContentSlivers
property to achieve a similar but more enhanced functionality.
// Before
WoltModalSheetPage.withCustomSliverList(sliverList: MySliverList());
// After
SliverWoltModalSheetPage(mainContentSlivers: [MySliverList1(), MySliverList2()]);
Getting started #
To use this plugin, add wolt_modal_sheet as a dependency in your pubspec.yaml file.
CupertinoApp support #
In the package, certain Material widgets rely on retrieving Material localizations information from the widget tree. However, Material localizations are not inherently available in CupertinoApp, leading to potential errors. To mitigate this issue, if your application utilizes CupertinoApp rather than MaterialApp, it is needed to incorporate a default Material localization delegate into your application configuration.
CupertinoApp(
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
],)
To see its usage, please check coffee maker example app.
Usage #
This package has 4 example projects.
Example app #
The example app demonstrates how to display a two-pages modal sheet that can be customized for dark and light themes using WoltModalSheetThemeData theme extension.
@override
Widget build(BuildContext context) {
final pageIndexNotifier = ValueNotifier(0);
SliverWoltModalSheetPage page1(BuildContext modalSheetContext, TextTheme textTheme) {
return WoltModalSheetPage(
hasSabGradient: false,
stickyActionBar: Padding(
padding: const EdgeInsets.all(_pagePadding),
child: Column(
children: [
ElevatedButton(
onPressed: () => Navigator.of(modalSheetContext).pop(),
child: const SizedBox(
height: _buttonHeight,
width: double.infinity,
child: Center(child: Text('Cancel')),
),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () => pageIndexNotifier.value = pageIndexNotifier.value + 1,
child: const SizedBox(
height: _buttonHeight,
width: double.infinity,
child: Center(child: Text('Next page')),
),
),
],
),
),
topBarTitle: Text('Pagination', style: textTheme.titleSmall),
isTopBarLayerAlwaysVisible: true,
trailingNavBarWidget: IconButton(
padding: const EdgeInsets.all(_pagePadding),
icon: const Icon(Icons.close),
onPressed: Navigator.of(modalSheetContext).pop,
),
child: const Padding(
padding: EdgeInsets.fromLTRB(
_pagePadding,
_pagePadding,
_pagePadding,
_bottomPaddingForButton,
),
child: Text(
'''
Pagination involves a sequence of screens the user navigates sequentially. We chose a lateral motion for these transitions. When proceeding forward, the next screen emerges from the right; moving backward, the screen reverts to its original position. We felt that sliding the next screen entirely from the right could be overly distracting. As a result, we decided to move and fade in the next page using 30% of the modal side.
''',
)),
);
}
SliverWoltModalSheetPage page2(BuildContext modalSheetContext, TextTheme textTheme) {
return SliverWoltModalSheetPage(
pageTitle: Padding(
padding: const EdgeInsets.all(_pagePadding),
child: Text(
'Material Colors',
style: textTheme.headlineMedium!.copyWith(fontWeight: FontWeight.bold),
),
),
heroImage: Image(
image: AssetImage(
'lib/assets/images/material_colors_hero${_isLightTheme ? '_light' : '_dark'}.png',
),
fit: BoxFit.cover,
),
leadingNavBarWidget: IconButton(
padding: const EdgeInsets.all(_pagePadding),
icon: const Icon(Icons.arrow_back_rounded),
onPressed: () => pageIndexNotifier.value = pageIndexNotifier.value - 1,
),
trailingNavBarWidget: IconButton(
padding: const EdgeInsets.all(_pagePadding),
icon: const Icon(Icons.close),
onPressed: () {
Navigator.of(modalSheetContext).pop();
pageIndexNotifier.value = 0;
},
),
mainContentSlivers: [
SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 2.0,
),
delegate: SliverChildBuilderDelegate(
(_, index) => ColorTile(color: materialColorsInGrid[index]),
childCount: materialColorsInGrid.length,
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(_, index) => ColorTile(color: materialColorsInSliverList[index]),
childCount: materialColorsInSliverList.length,
),
),
...materialColorsInSpinner.map((e) => Shifter(child: ColorTile(color: e))).toList(),
SliverPadding(
padding: const EdgeInsets.all(_pagePadding),
sliver: SliverToBoxAdapter(
child: TextButton(
onPressed: Navigator.of(modalSheetContext).pop,
child: const Text('Close'),
),
),
),
],
);
}
return MaterialApp(
themeMode: _isLightTheme ? ThemeMode.light : ThemeMode.dark,
theme: ThemeData.light(useMaterial3: true).copyWith(
extensions: const <ThemeExtension>[
WoltModalSheetThemeData(
heroImageHeight: _heroImageHeight,
topBarShadowColor: _lightThemeShadowColor,
modalBarrierColor: Colors.black54,
),
],
),
darkTheme: ThemeData.dark(useMaterial3: true).copyWith(
extensions: const <ThemeExtension>[
WoltModalSheetThemeData(
topBarShadowColor: _darkThemeShadowColor,
modalBarrierColor: Colors.white12,
sabGradientColor: _darkSabGradientColor,
dialogShape: BeveledRectangleBorder(),
bottomSheetShape: BeveledRectangleBorder(),
),
],
),
home: Scaffold(
body: Builder(
builder: (context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Light Theme'),
Padding(
padding: const EdgeInsets.all(_pagePadding),
child: Switch(
value: !_isLightTheme,
onChanged: (_) => setState(() => _isLightTheme = !_isLightTheme),
),
),
const Text('Dark Theme'),
],
),
ElevatedButton(
onPressed: () {
WoltModalSheet.show<void>(
pageIndexNotifier: pageIndexNotifier,
context: context,
pageListBuilder: (modalSheetContext) {
final textTheme = Theme.of(context).textTheme;
return [
page1(modalSheetContext, textTheme),
page2(modalSheetContext, textTheme),
];
},
modalTypeBuilder: (context) {
final size = MediaQuery.of(context).size.width;
if (size < _pageBreakpoint) {
return WoltModalType.bottomSheet;
} else {
return WoltModalType.dialog;
}
},
onModalDismissedWithBarrierTap: () {
debugPrint('Closed modal sheet with barrier tap');
Navigator.of(context).pop();
pageIndexNotifier.value = 0;
},
maxDialogWidth: 560,
minDialogWidth: 400,
minPageHeight: 0.0,
maxPageHeight: 0.9,
);
},
child: const SizedBox(
height: _buttonHeight,
width: _buttonWidth,
child: Center(child: Text('Show Modal Sheet')),
),
),
],
);
},
),
),
);
}
The code snippet above produces the following:
Playground app with imperative navigation #
The playground app demonstrates how to imperatively show the modal sheet. The purpose of this module is to play and experiment with various use cases. These use cases include:
- A page with a height set to be maximum regardless of the content height.
- A page with a hero image
- A page with a list whose items are lazily built.
- A page with an auto-focused text field.
- A page with a custom top bar.
- A page without a page title nor a top bar.
- A page whose properties are dynamically set.
- All the pages in one flow.
Playground app with declarative navigation #
The playground_navigator2 has the same content with the playground app but the modal sheet is shown using Navigator 2.0 (Router API) in a declarative way.
Coffee maker app for state management example #
Finally, the coffee_maker app demonstrates how to manage the state among the page components with an opinionated use of the Provider state management library.
The code snippet demonstrates how to decorate the modal sheet with a change notifier provider so that the page components can be rebuilt according to the current state:
void _onCoffeeOrderSelectedInAddWaterState(BuildContext context,
String coffeeOrderId) {
final model = context.read<StoreOnlineViewModel>();
final pageIndexNotifier = ValueNotifier(0);
WoltModalSheet.show(
pageIndexNotifier: pageIndexNotifier,
context: context,
decorator: (child) {
return ChangeNotifierProvider<StoreOnlineViewModel>.value(
value: model,
builder: (_, __) => child,
);
},
pageListBuilderNotifier: AddWaterModalPageBuilder.build(
coffeeOrderId: coffeeOrderId,
goToPreviousPage: () =>
pageIndexNotifier.value = pageIndexNotifier.value - 1,
goToNextPage: () => pageIndexNotifier.value = pageIndexNotifier.value + 1,
),
modalTypeBuilder: _modalTypeBuilder,
);
}
Additional information #
- Design Philosophy: Dive into the creative thought process behind WoltModalSheet's functionality in our blog post . Explore how we tackled the design challenges to create an intuitive and responsive experience.
- Insights from FlutterCon'23 talk: We delved into both the design and developmental facets of this package at the FlutterCon'23 conference. Catch the enlightening recording of his talk to understand the nuances.
- Flutter&Friends talk: This is a lightening talk given at the Flutter&Friends conference on September'23. It covers the design guidelines and best practices by showing real-world examples highlighting what to do—and what not to do. It also covers the technical details of the implementation. The recording of the talk can be found here.