3

Is there a way to use a SnackBar with a CupertinoPageScaffold?

I am getting the following error:

Scaffold.of() called with a context that does not contain a Scaffold.
No Scaffold ancestor could be found starting from the context that was passed to Scaffold.of().

Invoking a SnackBar in a child widget using:

final snackBar = SnackBar(
      content: Text('Yay! A SnackBar!'),
      action: SnackBarAction(
      label: 'Undo',
      onPressed: () {
      // Some code to undo the change!
      },
      ),
      );

// Find the Scaffold in the Widget tree and use it to show a SnackBar!
Scaffold.of(context).showSnackBar(snackBar);
Alan
  • 9,331
  • 14
  • 52
  • 97
  • where are you calling the `showSnackBar` inside your code hierarchy ? – Mazin Ibrahim Mar 27 '19 at 17:05
  • @MazinIbrahim because I am invoking it `onTap` as shown in Flutter's example, https://flutter.dev/docs/cookbook/design/snackbars#1-create-a-scaffold – Alan Mar 27 '19 at 17:07
  • Did you try the complete example at the end of the page https://flutter.dev/docs/cookbook/design/snackbars#1-create-a-scaffold – Mazin Ibrahim Mar 27 '19 at 17:42
  • 1
    @MazinIbrahim yep, but I'm using a `CupertinoPageScaffold` instead of a material `Scaffold` – Alan Mar 27 '19 at 17:48

3 Answers3

4

You need to include a Scaffold as CupertinoPageScaffold isn't a child of it, then you need to separate the code where you call the showSnackBar function from that of Scaffold into a separate class which is here the SnackBarBody because a SnackBar and a Scaffold can't be called from the same Build function. Here's a complete working example:

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';

void main() => runApp(SnackBarDemo());

class SnackBarDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
     title: 'SnackBar Demo',
     home: CupertinoPageScaffold(
    child: SnackBarPage(),
       ),
     );
   }
 }

class SnackBarPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
   body: SnackBarBody(),
   );
 }
}

class SnackBarBody extends StatefulWidget {
  SnackBarBody({Key key,}):
      super(key: key);

  @override
  _SnackBarBodyState createState() => new _SnackBarBodyState();
}

class _SnackBarBodyState extends State<SnackBarBody> {

@override
Widget build(BuildContext context) {
  return Center(
    child: RaisedButton(
      onPressed: () {
        final snackBar = SnackBar(content: Text('Yay! A SnackBar!'),action: SnackBarAction(label: 'Undo',
          onPressed: () {
            // Some code to undo the change!
          },
        ),
      );

        // Find the Scaffold in the Widget tree and use it to show a SnackBar!
        Scaffold.of(context).showSnackBar(snackBar);
       },
      child: Text('Show SnackBar'),
     ),
   );
  }
}
Mazin Ibrahim
  • 7,433
  • 2
  • 33
  • 40
  • I am not sure if this is the ideal solution. Flutter recommends against nesting `Scaffold`s as it can cause unintended behaviors. Seems like I cannot use a snackbar if I am using a `Cupertino` based app. – Alan Apr 02 '19 at 14:58
  • 1
    I agree with you Alan. If you badly need a `Scaffold` I think it is the only available method. – Mazin Ibrahim Apr 02 '19 at 16:21
2

Cupertino widgets should be specific for iOS, SnackBar come to replace Android Toasts but there is no Android Toast equivalent in iOS, so you cannot natively add a SnackBar to a CupertinoScaffold as it will not follow iOS guidelines.

One solution will be to extend CupertinoScaffold and add it the SnackBar code.

An other is in Mazin Ibrahim answer, note that it isn't recommended to nest Scaffolds.

Milvintsiss
  • 1,420
  • 1
  • 18
  • 34
  • Thanks for the answer. Definitely looking to avoid nesting scaffolds. Is extending a Flutter widget a common pattern? What's the most common approach for showing snackbar-like or toast-like messages on iOS via the `CupertinoPageScaffold`? Or is `CupertinoPageScaffold` just not really suited for timed messages, and just use `Scaffold`? Trying to learn and integrate Cupertino with Material, but various aspects seem a bit restrictive. – Keith DC Jun 04 '21 at 23:06
  • Yes, Flutter widgets doesn't give what you want extending it is a great idea. I don't really use much Cupertino Widgets so I don't know what is the common behavior for this, maybe using `CupertinoAlertDiaolg` with a timer to make them diseapear? But this will freeze the ui... I think using `Scaffold` is your best possible approch if you don't want to take a lot of time for this. `Scaffold` isn't a lot different from a `CupertinoScaffold` and I don't think you will have trouble to make `Scaffold` look like a `CupertinoScaffold` – Milvintsiss Jun 05 '21 at 00:17
2

Although, it is not possible to use showSnackBar with CupertinoPageScaffold, but you can create customisable SnackBar like feature for iOS (CupertinoPageScaffold) by making use of OverlayEntry.

Because an Overlay uses a Stack layout, overlay entries can use Positioned and AnimatedPositioned to position themselves within the overlay.

Here, I've created a function showCupertinoSnackBar for easier use:

import 'package:flutter/cupertino.dart';

void showCupertinoSnackBar({
  required BuildContext context,
  required String message,
  int durationMillis = 3000,
}) {
  final overlayEntry = OverlayEntry(
    builder: (context) => Positioned(
      bottom: 8.0,
      left: 8.0,
      right: 8.0,
      child: CupertinoPopupSurface(
        child: Padding(
          padding: const EdgeInsets.symmetric(
            horizontal: 8.0,
            vertical: 8.0,
          ),
          child: Text(
            widget.message,
            style: TextStyle(
              fontSize: 14.0,
              color: CupertinoColors.secondaryLabel,
            ),
            textAlign: TextAlign.center,
          ),
        ),
      ),
    ),
  );
  Future.delayed(
    Duration(milliseconds: durationMillis),
    overlayEntry.remove,
  );
  Overlay.of(Navigator.of(context).context)?.insert(overlayEntry);
}

You can customise the SnackBar to your liking. If you need to animate the overlay, here's another example using AnimatedPositioned inside a stateful widget:

import 'package:flutter/cupertino.dart';

void showCupertinoSnackBar({
  required BuildContext context,
  required String message,
  int durationMillis = 3000,
}) {
  const animationDurationMillis = 200;
  final overlayEntry = OverlayEntry(
    builder: (context) => _CupertinoSnackBar(
      message: message,
      animationDurationMillis: animationDurationMillis,
      waitDurationMillis: durationMillis,
    ),
  );
  Future.delayed(
    Duration(milliseconds: durationMillis + 2 * animationDurationMillis),
    overlayEntry.remove,
  );
  Overlay.of(Navigator.of(context).context)?.insert(overlayEntry);
}

class _CupertinoSnackBar extends StatefulWidget {
  final String message;
  final int animationDurationMillis;
  final int waitDurationMillis;

  const _CupertinoSnackBar({
    Key? key,
    required this.message,
    required this.animationDurationMillis,
    required this.waitDurationMillis,
  }) : super(key: key);

  @override
  State<_CupertinoSnackBar> createState() => _CupertinoSnackBarState();
}

class _CupertinoSnackBarState extends State<_CupertinoSnackBar> {
  bool _show = false;

  @override
  void initState() {
    super.initState();
    Future.microtask(() => setState(() => _show = true));
    Future.delayed(
      Duration(
        milliseconds: widget.waitDurationMillis,
      ),
      () {
        if (mounted) {
          setState(() => _show = false);
        }
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedPositioned(
      bottom: _show ? 8.0 : -50.0,
      left: 8.0,
      right: 8.0,
      curve: _show ? Curves.linearToEaseOut : Curves.easeInToLinear,
      duration: Duration(milliseconds: widget.animationDurationMillis),
      child: CupertinoPopupSurface(
        child: Padding(
          padding: const EdgeInsets.symmetric(
            horizontal: 8.0,
            vertical: 8.0,
          ),
          child: Text(
            widget.message,
            style: TextStyle(
              fontSize: 14.0,
              color: CupertinoColors.secondaryLabel,
            ),
            textAlign: TextAlign.center,
          ),
        ),
      ),
    );
  }
}
rmalviya
  • 1,847
  • 12
  • 39
  • Thanks for this. I was surprised that its so difficult to use a snackbar when using Cupertino widgets. I get that its not native to iOS, but it certainly wouldn't hurt to have an easier platform independent snackbar solution in the Flutter SDK. – Loren.A Apr 04 '23 at 14:16
  • The Only Correction in the Above solution is - in showCupertinoSnackBar , -- Replace Overlay.of(Navigator.of(context).context)?.insert(overlayEntry) -- to Overlay.of(context).insert(overlayEntry) – Mohammad Farseen Jul 28 '23 at 05:27