funwithflutter/flutter_confetti

Not shown in release mode

Opened this issue ยท 16 comments

As the title says, I don't get the effect at release mode, but during debug mode, it works ok.
My code is pretty much the same as the example, in my case a use provider to use the ConfettiControlle deep in the widget tree. It goes like this.

main.dart

ListenableProvider<ConfettiController>.value(
              value: controllerTopCenter,
              child: HomePage(),
            ),

video.dart

//Flutter and Dart import
import 'dart:async';
import 'package:confetti/confetti.dart';
import 'package:flutter/material.dart';
import 'package:flutter_icons/simple_line_icons.dart';

//Self import
import 'package:Tekel/core/model/riddle.dart';
import 'package:Tekel/core/viewModel/videoViewModel.dart';
import 'package:provider/provider.dart';

class VideoLayaout extends StatefulWidget {
  final Riddle riddle;
  final VideoViewModel model;
  final Stream shouldTriggerChange;

  VideoLayaout({this.riddle, this.model, this.shouldTriggerChange});

  @override
  _VideoLayaoutState createState() => _VideoLayaoutState();
}

class _VideoLayaoutState extends State<VideoLayaout>
    with TickerProviderStateMixin {
  StreamSubscription streamSubscription;
  AnimationController fadeController;
  Animation fadeAnimation;

  @override
  initState() {
    super.initState();
    streamSubscription =
        widget.shouldTriggerChange.listen((value) => success(value));
    fadeController =
        AnimationController(vsync: this, duration: Duration(seconds: 1));
    fadeAnimation = Tween(begin: 0.0, end: 1.0).animate(fadeController);
  }

  @override
  void dispose() {
    widget.model.videoController?.dispose();
    streamSubscription.cancel();
    fadeController.dispose();
    super.dispose();
  }

  void success(value) async {
    if (value == true) {
      Future.delayed(
        Duration.zero,
        () => setState(
          () {
            fadeController.forward();
          },
        ),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Stack(
        fit: StackFit.loose,
        children: <Widget>[
          FutureBuilder(
            future: widget.model.getMedia(),
            builder: (BuildContext context, AsyncSnapshot snapshot) {
              switch (snapshot.connectionState) {
                case ConnectionState.none:
                  return Text('Check your network connection.');
                case ConnectionState.active:
                case ConnectionState.waiting:
                case ConnectionState.done:
                  if (snapshot.hasError)
                    return Text('Error: try later, please');
                  return Padding(
                    padding: const EdgeInsets.all(10.0),
                    child: Center(child: widget.model.widget),
                  );
              }
              return Text('Unreachable.');
            },
          ),
          FadeTransition(
              opacity: fadeAnimation, child: buildSuccessContainer()),
        ],
      ),
    );
  }

  Container buildSuccessContainer() {
    fadeController.isAnimating
        ? Provider.of<ConfettiController>(context).play()
        : null;
    return Container(
      padding: EdgeInsets.all(10.0),
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: [Colors.yellow[600], Colors.orange[400]],
          begin: const FractionalOffset(0.0, 0.0),
          end: const FractionalOffset(1, 0.0),
          stops: [0.0, 1.0],
          tileMode: TileMode.clamp,
        ),
      ),
      child: Center(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Icon(
              SimpleLineIcons.getIconData('check'),
              color: Colors.black,
              size: 40.0,
            ),
            SizedBox(
              height: 20.0,
            ),
            Text('Success',
                style: TextStyle(color: Colors.black, fontSize: 40.0)),
          ],
        ),
      ),
    );
  }
}

Beforehand, thanks for your time.

I did a quick test and on my side the package is working in release mode. I don't believe that is the issue.

Other things that might be happening are:

  • Some Assert logic in your code base that only triggers on debug builds that results in the confetti_controller not playing
  • The confetti_controller gets disposed before use (if that is the case there should be error logs)

Could you please share you ConfettiWidget implementation and error logs running in release mode.

I was doing some manual test at release mode, I found out that the controller is playing, particle sometimes appears, and then almost immediately disappear at the top center.

If triggers the confetti controller, do a Navigator.pushReplacementNamed after that come back to the previous page, confetti starts to play.

I think that for some reason the screen is not been updating to let the controller play.

I do not get any error logs.

void main() {
  setPortraitOrientation();
  return runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  ConfettiController controllerTopCenter;

  @override
  void initState() {
    controllerTopCenter = ConfettiController(duration: Duration(seconds: 10));
    super.initState();
  }

  @override
  void dispose() {
    controllerTopCenter.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: providers,
      child: MaterialApp(
        title: 'Tekel',
        theme: costumTheme,
        home: Stack(
          children: <Widget>[
            ListenableProvider<ConfettiController>.value(
              value: controllerTopCenter,
              child: HomePage(),
            ),
            Align(
              alignment: Alignment.topCenter,
              child: ConfettiWidget(
                confettiController: controllerTopCenter,
                blastDirection: pi / 2,
                maxBlastForce: 10,
                minBlastForce: 2,
                emissionFrequency: 0.05,
                numberOfParticles: 5,
              ),
            ),
          ],
        ),
        onGenerateRoute: router.generateRoute,
        initialRoute: '/',
      ),
    );
  }
}

UPDATE:
I added the confetti Controller.play () to my stream.listen (), and now it works perfectly fine. I just don't clary understand with my last implementation work at debug mode. Anyway thanks for your time.

Perfect! If you figure it out drop a message. I'm closing this issue for now.

Yes, it is solved. Thanks.

I am facing mostly the same problem: works fine on debug, mostly not showing on release.
On debug it works just as expected. But with the release build, it shows only one blinking confetti in the shooting point.

Other things that might be happening are:

  • Some Assert logic in your code base that only triggers on debug builds that results in the confetti_controller not playing
  • The confetti_controller gets disposed before use (if that is the case there should be error logs)

checked this. looks like not my case.
here some part of my code:

  @override
  void initState() {
    _controllerCenter = ConfettiController(duration: const Duration(seconds: 3));
    WidgetsBinding.instance.addPostFrameCallback((_) {
        _controllerCenter.play();   
    });
    super.initState();
  }

  Widget _createIcon() {
    const size = 150.0;
    final stack = Stack(
      children: [
          SvgWidget.asset(_saluteIconPath, width: size, height: size),        
          Center(child: _createConfetti()),
      ],
    );
    return SizedBox(
      width: size,
      height: size,
      child: stack,
    );
  }

  Widget _createConfetti() => ConfettiWidget(
        confettiController: _controllerCenter,
        blastDirection: -pi / 4,
        emissionFrequency: 0.8,
        minimumSize: const Size(10, 3),
        maximumSize: const Size(20, 5),
        numberOfParticles: 2,
        gravity: 0.02,
        maxBlastForce: 3,
        minBlastForce: 1,
      );

will appreciate any ideas.
In case of need can add a gif video with an examples.

@funwithflutter can you please give me any hint/help with my problem?

some extra details:

  • works fine on android emultator
  • work with bug described above on android real device

@pro100svitlo I've run into times when release mode won't show something while in debug it does show (not related to this package). And normally for me it happens because of a Stack issue. Make sure that the widgets in your stack are defining a size. For example, wrap the widget in Positioned.fill to ensure it takes up all the available space.

You can also try to simplify what you are doing and see if it works under other conditions (for release mode). But it sounds like the confetti is being destroyed as soon as they are created. So it seems like the confetti widget doesn't have enough "space". You can also try wrapping it in a Container with a fixed size and you'll see what I mean. It should destroy the confetti when it leaves that defined "area/space/size"

I'm having a similar issue that only happens when I run a release build on my iPhone. When I open the ConfettiView within showModalBottomSheet, the confetti flashes in the center of the modal instead of expanding outward.

Confetti animates correctly (on device in release build) when

  • I press the button in the modal that triggers _controller.play()
  • the ConfettiView is not in a showModalBottomSheet and _controller.play() is called by initState

Confetti doesn't animate correctly (on device in release build) when

  • the ConfettiView is in a showModalBottomSheet and _controller.play() is called by initState, even when using addPostFrameCallback

@funwithflutter Any idea why this happens with on device release builds when _controller.play() is called in initState, but not when _controller.play() is triggered by a button press?

Minimal example:

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

void main() => runApp(const ConfettiSample());

class ConfettiSample extends StatelessWidget {
  const ConfettiSample({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Builder(
        builder: (context) => Scaffold(
          body: Center(
            child: FlatButton(
              child: Text('open confetti in modal view'),
              onPressed: () {
                showModalBottomSheet(
                  context: context,
                  builder: (BuildContext bc) {
                    return ConfettiView();
                  },
                );
              },
            ),
          ),
        ),
      ),
    );
  }
}

class ConfettiView extends StatefulWidget {
  @override
  _ConfettiViewState createState() => _ConfettiViewState();
}

class _ConfettiViewState extends State<ConfettiView> {
  ConfettiController _controller;

  @override
  void initState() {
    _controller = ConfettiController(duration: const Duration(seconds: 3));
    _controller.play(); // <-- This causes the confetti to get stuck in one location and flash (when in a showModalBottomSheet)
    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: <Widget>[
          Align(
            alignment: Alignment.center,
            child: ConfettiWidget(
              confettiController: _controller,
              blastDirectionality: BlastDirectionality.explosive,
            ),
          ),
          Align(
            alignment: Alignment.center,
            child: FlatButton(
              onPressed: () => _controller.play(), // <-- This works as expected
              child: Text('blast\nconfetti'),
            ),
          ),
        ],
      ),
    );
  }
}

@ateich I have an idea of what the issue could be. I'll take a look at this next week.

Any chance you had the time to dig into this issue @funwithflutter?

@funwithflutter what about this issue? No changes?

@funwithflutter I think issue was with RepaintBoundary in

return RepaintBoundary(
child: CustomPaint(
key: _particleSystemKey,
foregroundPainter: ParticlePainter(
_animController,
particles: _particleSystem.particles,
paintEmitterTarget: widget.displayTarget,
),
child: widget.child,
),
);

I made a fork and removed it https://github.com/deep-gaurav/flutter_confetti . Seems to have fixed it, not sure why though.

I had the same issue too: when working on the single screen using the lib, it worked like a charm. But when testing it in a real-world app, I add all confetti in the same place. I think I might have a clue, however: I use auto_route in my app, and I think it is what causes the problem.

I did find a workaround as if I display my confetti screen:

  • like all other screens of my app using auto_route's context.pushRoute method, I have the bug
  • as an overlay that I just don't hide anymore when it needs to show, it works fine

I wonder if this might not be related to the animation/transition when a new route is displayed?
Could it be something similar for other people that got the issue? Thanks and let me know if I can give more information!

For me, it is because I used minimum / maximum size based on screen dimension. Partices are not showing at all in release mode. Though, removing minimum/maximumSize or using constant size values (e.g. Size(10, 10)) will fix it.

@override
Widget build(BuildContext context) {
  final confettiSize = MediaQuery.of(context).size.shortestSide * 0.03;
  return Stack(
    children: [
      widget.child,
      Align(
        alignment: const Alignment(0, -0.5),
        child: ConfettiWidget(
          confettiController: _controller,
          blastDirectionality: BlastDirectionality.explosive,
          maxBlastForce: 20,
          minBlastForce: 1,
          emissionFrequency: 0.1,
          numberOfParticles: 50,
          gravity: 0.05,
          minimumSize: Size.square((confettiSize * 0.5).truncateToDouble()), // replace or remove
          maximumSize: Size.square(confettiSize.truncateToDouble()), // replace or remove
        ),
      ),
    ],
  );
}

Hey, I have found a fix! Just change the _isOutsideOfBorder method in the particle.dart file to this:

bool _isOutsideOfBorder(Offset particleLocation) {
    final globalParticlePosition = particleLocation + _particleSystemPosition!;
    
    return (globalParticlePosition.dy >= _bottomBorder);
  }

This will maybe impact performance very lightly, because now particles only get deleted after they have passed the bottom of the screen, not the left or right.