• Stars
    star
    318
  • Rank 126,982 (Top 3 %)
  • Language
    Dart
  • License
    MIT License
  • Created 11 months ago
  • Updated 4 days ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

This package provides a responsive modal with multiple pages, motion animation for page transitions, and scrollable content within each page.

Wolt Wolt on pub.dev
Platforms
Pub Version Pub points Pub Likes Pub popularity
Repo stars Repo PRs Repo issues Contributors License
Coverage Status

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.

Experience multi-page navigation in WoltModalSheet

Scrollable Content

Greater flexibility with scrollable content per page, accommodating large content effortlessly.

Scroll with ease in WoltModalSheet

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.

Adaptability to different screen sizes in WoltModalSheet

Motion Animation

Engage users with dynamic motion animation for page transitions and scrolling.

Pagination Scrolling
Pagination Scrolling

Imperative and Declarative Navigation

The library showcases examples of both imperative and declarative navigation patterns to display modal sheet on screen.

Illustration of imperative and declarative navigation in WoltModalSheet

Dynamic Pagination

User input can dynamically shape the modal sheet's page list.

Dynamic pagination in action in WoltModalSheet

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:

  • Main Content Layer: The fundamental content of the page, including the optional page title, optional hero image, and the main content, which may be scrollable.
  • Top Bar Layer: Further above the main content layer, this layer with the filled color includes the top bar title and may become hidden or sticky based on scroll position and specific properties.
  • Navigation Bar Layer: Sitting at the top of the top bar layer on z-axis, this transparent-background layer contains navigational widgets for the interface, such as back or close buttons.
  • Sticky Action Bar Layer: Positioned at the top of the z axis, this layer guides the user towards the next step, uses an optional gentle gradient on top to hint that there is more content below ready for scrolling.

  • Modal sheet page layers

    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.

    Modal sheet elements breakdown

    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:

    Modal sheet elements in use

    Getting started

    To use this plugin, add wolt_modal_sheet as a dependency in your pubspec.yaml file.

    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.

    Widget build(BuildContext context) {
      final pageIndexNotifier = ValueNotifier(0);
    
      WoltModalSheetPage page1(BuildContext modalSheetContext, TextTheme textTheme) {
        return WoltModalSheetPage.withSingleChild(
          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.
    ''',
                  )),
        );
      }
    
      WoltModalSheetPage page2(BuildContext modalSheetContext, TextTheme textTheme) {
        return WoltModalSheetPage.withCustomSliverList(
          stickyActionBar: Padding(
            padding:
            const EdgeInsets.fromLTRB(_pagePadding, _pagePadding / 4, _pagePadding, _pagePadding),
            child: ElevatedButton(
              onPressed: () {
                Navigator.of(modalSheetContext).pop();
                pageIndexNotifier.value = 0;
              },
              child: const SizedBox(
                height: _buttonHeight,
                width: double.infinity,
                child: Center(child: Text('Close')),
              ),
            ),
          ),
          pageTitle: Padding(
            padding: const EdgeInsets.symmetric(horizontal: _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;
            },
          ),
          sliverList: SliverList(
            delegate: SliverChildBuilderDelegate(
                      (_, index) => ColorTile(color: allMaterialColors[index]),
              childCount: allMaterialColors.length,
            ),
          ),
        );
      }
    
      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:

    Example app

    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,
      );
    }

    Dynamic pagination in action in WoltModalSheet

    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.

    Contributing

    To get started with contributing, please follow the steps below:

    1. Fork the wolt_modal_sheet repo on GitHub.
    2. Clone your forked repo locally.
    3. Ensure you have Melos installed.
      dart pub global activate melos
    4. Use Melos to bootstrap the project.
      melos bootstrap
    5. Create a new branch from the main branch.
    6. Make your changes.
    7. Create a pull request.

    More Repositories

    1

    blurhash

    A very compact representation of a placeholder for an image.
    C
    14,860
    star
    2

    react-blurhash

    React components for blurhash
    TypeScript
    558
    star
    3

    wolt-python-package-cookiecutter

    Cookiecutter for rapidly creating modern & high-quality Python packages
    Python
    238
    star
    4

    blurhash-python

    Python version of the BlurHash encoder
    Python
    151
    star
    5

    mitmproxy-mock

    A tool to mock/modify server responses easily with mitmproxy
    Python
    87
    star
    6

    redux-autoloader

    A higher order component for declarative data loading in React and Redux.
    JavaScript
    57
    star
    7

    engineering-internship-2024

    The pre-assignment for frontend / backend internship applicants
    51
    star
    8

    wolt_responsive_layout_grid

    Wolt Responsive Layout Grid library introduces the Flutter implementation of Material Design's responsive layout grid. It provides a unified, multi-platform grid system that ensures consistency and visual integrity regardless of the device or operating system being used.
    Dart
    49
    star
    9

    parallelpbf

    OpenStreetMap PBF format multithreaded reader
    Java
    46
    star
    10

    engineering-summer-intern-2023

    34
    star
    11

    arrow-detekt-rules

    Kotlin
    28
    star
    12

    magic-di

    Dependency Injector with minimal boilerplate code, built-in support for FastAPI and Celery, and seamless integration to basically anything.
    Python
    27
    star
    13

    engineering-summer-intern-2022

    The home assignment for the Wolt Engineering Summer Internships 2022
    25
    star
    14

    react-geoinput

    Geolocation suggestions and coordinates with Google Maps API for React
    JavaScript
    20
    star
    15

    react-router-query-params

    react-router-query-params
    JavaScript
    19
    star
    16

    summer2021-internship

    Wolt Summer 2021 Internships - Preliminary Assignment for Engineering Positions
    19
    star
    17

    data-science-summer-intern-2021

    Assignment for Data Science Summer Intern candidates 2021
    18
    star
    18

    spark-osm-datasource

    Native Spark OSM PBF data source
    Scala
    15
    star
    19

    summer2020

    Assignment for engineering intern positions
    14
    star
    20

    data-science-internship-2024

    The pre-assignment for data science internship applicants
    14
    star
    21

    python-fastapi-workshop

    Materials for "Modern Python APIs with FastAPI"
    Python
    12
    star
    22

    celery-farmer

    Python
    11
    star
    23

    react-native-assignment

    React Native UI programming assignment
    JavaScript
    11
    star
    24

    analytics-summer-intern-2022

    Assignment for Analytics Service Summer Intern candidates for 2022
    7
    star
    25

    summer2018

    Coding task
    6
    star
    26

    data-science-summer-intern-2022

    6
    star
    27

    mobile-engineering-internship-2024

    The pre-assignment for mobile (Flutter) internship applicants
    5
    star
    28

    summer2019

    Coding task for summer interns 2019
    5
    star
    29

    memories

    Links and description of tools together with a play project for exploring memory problems
    Kotlin
    4
    star
    30

    spark-osm-tools

    Scala
    3
    star
    31

    junction-2022-materials

    Materials for the Wolt's Junction 2022 challenge
    2
    star
    32

    looker-viz-transposed-table

    2
    star