darioielardi / flutter_speed_dial

Flutter plugin to implement a Material Design Speed Dial
https://pub.dev/packages/flutter_speed_dial
MIT License
416 stars 178 forks source link

Do not check for _open when disposing the SpeedDial #286

Closed rostaingc closed 1 year ago

rostaingc commented 1 year ago

When the SpeedDial's child's action changes the state, it could lead to the SpeedDial no longer being displayed (I have created a simple example lower).

When this happens, an exception is thrown. In speed_dial.dart, in the SpeedDial dispose function, backgroundOverlay!.remove is only called if _open is true. But this is not the case in this example. In this scenario, backgroundOverlay!.dispose() is called yet the backgroundOverlay!.removed() was not.

  @override
  void dispose() {
    if (widget.renderOverlay && backgroundOverlay != null) {
      if (_open && backgroundOverlay!.mounted) backgroundOverlay!.remove();
      backgroundOverlay!.dispose();
    }
    if (overlayEntry != null) {
      if (_open && overlayEntry!.mounted) overlayEntry!.remove();
      overlayEntry!.dispose();
    }
    _controller.dispose();
    widget.openCloseDial?.removeListener(_onOpenCloseDial);
    super.dispose();
  }

Exception thrown:

════════ Exception caught by widgets library ═══════════════════════════════════
The following assertion was thrown while finalizing the widget tree:
An OverlayEntry must first be removed from the Overlay before dispose is called.
'package:flutter/src/widgets/overlay.dart':
package:flutter/…/widgets/overlay.dart:1
Failed assertion: line 210 pos 12: '_overlay == null'

When the exception was thrown, this was the stack
#2      OverlayEntry.dispose
package:flutter/…/widgets/overlay.dart:210
#3      _SpeedDialState.dispose
package:flutter_speed_dial/src/speed_dial.dart:250
#4      StatefulElement.unmount
package:flutter/…/widgets/framework.dart:5105
#5      _InactiveElements._unmount
package:flutter/…/widgets/framework.dart:1917
#6      ListIterable.forEach (dart:_internal/iterable.dart:39:13)
#7      _InactiveElements._unmountAll
package:flutter/…/widgets/framework.dart:1926
#8      BuildOwner.lockState
package:flutter/…/widgets/framework.dart:2523
#9      BuildOwner.finalizeTree
package:flutter/…/widgets/framework.dart:2947
#10     WidgetsBinding.drawFrame
package:flutter/…/widgets/binding.dart:885
#11     RendererBinding._handlePersistentFrameCallback
package:flutter/…/rendering/binding.dart:378
#12     SchedulerBinding._invokeFrameCallback
package:flutter/…/scheduler/binding.dart:1175
#13     SchedulerBinding.handleDrawFrame
package:flutter/…/scheduler/binding.dart:1104
#14     SchedulerBinding._handleDrawFrame
package:flutter/…/scheduler/binding.dart:1015
#15     _invoke (dart:ui/hooks.dart:148:13)
#16     PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:318:5)
#17     _drawFrame (dart:ui/hooks.dart:115:31)
(elided 2 frames from class _AssertionError)
════════════════════════════════════════════════════════════════════════════════
Reloaded 2 of 655 libraries in 770ms (compile: 17 ms, reload: 374 ms, reassemble: 342 ms).

Simple reproducible example:

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

void main() {
  runApp(const SpeedDialPage());
}

class SpeedDialPage extends StatefulWidget {
  const SpeedDialPage({super.key});

  @override
  State<SpeedDialPage> createState() => _SpeedDialPageState();
}

class _SpeedDialPageState extends State<SpeedDialPage> {
  bool showSpeedDial = true;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: ElevatedButton(
            onPressed: () {
              setState(() {
                showSpeedDial = true;
              });
            },
            child: const Text('Reset'),
          ),
        ),
        floatingActionButton: showSpeedDial
            ? SpeedDial(
                children: [
                  SpeedDialChild(
                    child: const Text('hide'),
                    onTap: () {
                      setState(() {
                        showSpeedDial = false;
                      });
                    },
                  ),
                ],
                child: const Icon(Icons.edit),
              )
            : const SizedBox(),
      ),
    );
  }
}