Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EXCEPTION: FlutterError (Cannot get renderObject of inactive element. #455

Open
pamafe1976 opened this issue May 31, 2024 · 7 comments · May be fixed by #456
Open

EXCEPTION: FlutterError (Cannot get renderObject of inactive element. #455

pamafe1976 opened this issue May 31, 2024 · 7 comments · May be fixed by #456

Comments

@pamafe1976
Copy link

pamafe1976 commented May 31, 2024

Hi,

I've been working a couple days trying to isolate this problem, and I simplified it to a point where I know it appears with a combination of Showcaseview, Getx and ResponsiveSizer.
I hope someone can point me in the right direction to solve it.
I have made a small app to reproduce the problem I was having.

STEPS TO REPRODUCE
Start the test app.
Close the first tooltip.
Press button "ONE" to go to the seconds page
Press button "TWO" to return to the first page.
Minimize the app.

You will get an exception in framework.dart

FlutterError (Cannot get renderObject of inactive element.
In order for an element to have a valid renderObject, it must be active, which means it is part of the tree.
Instead, this element is in the _ElementLifecycle.inactive state.
If you called this method from a State object, consider guarding it with State.mounted.
The findRenderObject() method was called for the following element:
  LayoutBuilder)

FACTS:

  1. Only happens with the ResponsiveSizer as parent of GetMaterialApp, apparently because it rebuilds when the app goes to background.
  2. Only happens in IOS physical or simulator, probably because the build is not triggered in Android (Dont know why)
  3. The culprit seems to be showcase, when it calls "findRenderObject" I have checked that when the error happens, mounted is true, but the context is marked as detached and _lifecycleState is inactive, therefore the error
  4. Eventhough crashlytics reports this as a crash, the app is never closed, I guess because this happens when the apps goes to the background.
  5. Notice Im using a fork of showcaseview because I need it to work in Fluter 3.22, but I have checked that the issue is the same with flutter 3.19 and the original package

Thanks!

CODE TO REPRODUCE

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:showcaseview/showcaseview.dart';
import 'package:get/get.dart';
import 'package:responsive_sizer/responsive_sizer.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  runApp(const TestApp());
}

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

  @override
  State<TestApp> createState() => _TestAppState();
}

class _TestAppState extends State<TestApp> {
  @override
  Widget build(BuildContext context) {
    return ResponsiveSizer(
      builder: (context, orientation, sizingInformation) => GetMaterialApp(
        initialRoute:
            '/', //No cambiar por /mapa, dado que el inicio desde dynamic link dara error
        defaultTransition: Transition.native,
        getPages: [
          GetPage(
              name: '/',
              participatesInRootNavigator: true,
              page: () => const View1()),
          GetPage(name: '/two', page: () => const View2()),
        ],
      ),
    );
  }
}

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

  @override
  State<View1> createState() => _View1State();
}

class _View1State extends State<View1> {
  final keyOne = GlobalKey<ShowCaseWidgetState>();

  static bool firstTime = true;

  @override
  Widget build(BuildContext context) {
    return ShowCaseWidget(builder: Builder(builder: (cs) {
      if (firstTime) {
        firstTime = false;
        WidgetsBinding.instance.addPostFrameCallback((_) {
          ShowCaseWidget.of(cs).startShowCase([keyOne]);
        });
      }
      return Column(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          Showcase(
              key: keyOne,
              description: 'One',
              child: ElevatedButton(
                child: const Text('ONE'),
                onPressed: () {
                  Get.offAllNamed('/two');
                },
              )),
        ],
      );
    }));
  }
}

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

  @override
  State<View2> createState() => _View2State();
}

class _View2State extends State<View2> {

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: [
        ElevatedButton(
          child: const Text('TWO'),
          onPressed: () {
            Get.offAllNamed('/');
          },
        ),
      ],
    );
  }
}

PUBSPEC.YAML

name: testapp
description: "A new Flutter project."

publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 1.0.0+1

environment:
  sdk: '>=3.4.1 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.6
  responsive_sizer: ^3.3.1
  get: ^4.6.6
  showcaseview: 
    path: ../forks/flutter_showcaseview

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^3.0.0


flutter:
  uses-material-design: true

FLUTTER DOCTOR -V:

[✓] Flutter (Channel stable, 3.22.1, on macOS 14.5 23F79 darwin-arm64, locale es-AR)
    • Flutter version 3.22.1 on channel stable at /Users/pablo/Proyectos/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision a14f74ff3a (9 days ago), 2024-05-22 11:08:21 -0500
    • Engine revision 55eae6864b
    • Dart version 3.4.1
    • DevTools version 2.34.3

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    • Android SDK at /Users/pablo/Library/Android/sdk
    • Platform android-34, build-tools 34.0.0
    • ANDROID_HOME = /Users/pablo/Library/Android/sdk
    • ANDROID_SDK_ROOT = /Users/pablo/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 17.0.10+0-17.0.10b1087.21-11572160)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 15.4)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 15F31d
    • CocoaPods version 1.15.2

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2023.3)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.10+0-17.0.10b1087.21-11572160)

[✓] VS Code (version 1.89.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.91.20240529
@pamafe1976
Copy link
Author

pamafe1976 commented Jun 1, 2024

I have found this line in layout_overlays.dart be the one causing the problem, since context is detacched when it attempts findRenderObject.
final box = context.findRenderObject() as RenderBox;
However I dont know how to property guard it.

class AnchoredOverlay extends StatelessWidget {
  final bool showOverlay;
  final OverlayBuilderCallback? overlayBuilder;
  final Widget? child;
  final RenderObject? rootRenderObject;

  const AnchoredOverlay({
    super.key,
    this.showOverlay = false,
    this.overlayBuilder,
    this.child,
    this.rootRenderObject,
  });

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        return OverlayBuilder(
          showOverlay: showOverlay,
          overlayBuilder: (overlayContext) {

            final box = context.findRenderObject() as RenderBox;  // THIS LINE IS THE ONE CAUSING THE ERROR

            final topLeft = box.size.topLeft(
              box.localToGlobal(
                const Offset(0.0, 0.0),
                ancestor: rootRenderObject,
              ),
            );
            final bottomRight = box.size.bottomRight(
              box.localToGlobal(
                const Offset(0.0, 0.0),
                ancestor: rootRenderObject,
              ),
            );
            Rect anchorBounds;
            anchorBounds = (topLeft.dx.isNaN ||
                    topLeft.dy.isNaN ||
                    bottomRight.dx.isNaN ||
                    bottomRight.dy.isNaN)
                ? const Rect.fromLTRB(0.0, 0.0, 0.0, 0.0)
                : Rect.fromLTRB(
                    topLeft.dx,
                    topLeft.dy,
                    bottomRight.dx,
                    bottomRight.dy,
                  );
            final anchorCenter = box.size.center(topLeft);
            return overlayBuilder!(overlayContext, anchorBounds, anchorCenter);
          },
          child: child,
        );
      },
    );
  }
}

@mehrdad1990
Copy link

I know that somehow the problem is because of rootRenderObject being added here. I don't know why but for me everything works in showcaseview 2.0.3. and this rootRenderObject is something new. I think this is the cause

@pamafe1976
Copy link
Author

Hi @mehrdad1990. The rootRenderObject seems to be there since always. It fails in 2.0.3, I tested it.
The problem is not that easy to reproduce, because you have to trigger a rebuild when the app has gone to background in IOS. Maybe thats something that should not happen and its a bug in my app, I dont know, but any case, the showcase should complete the build without throwing, like all other widgets.
The code I posted reproduces this issue.
Regards

@mehrdad1990
Copy link

@pamafe1976
what happens if you remove showcaseview as parent of your widget when you trigger a rebuild? no error?
for me I had a throw-error with localToGlobal method call in showcaseview in the latest releases. but there was no error in 2.0.3 and I checked the code differences and I thought maybe it is because of rootRenderObject. maybe I'm wrong I'm not sure.
one solution that I did was to remove showcaseview as parent using a boolean when I'm done with it.

@pamafe1976
Copy link
Author

@mehrdad1990
The problem is with rootRenderObject, specially the one that is inside the GetPosition class.
For what I have learned, after app goes to background, IOS will trigger several builds, to make image snapshots of the app in the launcher (probably for landscape and portrait modes, and also dark and light themes) In that builds anything accessing context gets rebuilt. Checking State.mounted in that situation usually returns true, but the context is marked as detached, as seen by context.toString. When findRenderObject is called with such a context, _lifecycleState returns inactive, and therefore the framework throws the error.

Conditionally removing the ShowCaseWidget is not the best solution, since the user can send the app to back before the showcase has ended, and that would anyway trigger the error.

I have created an issue in Flutter just to check if the IOS build calls are normal.

But for sure Showcase and ShowCaseWidget should be able to build in that situation, as every other widget does.

Regards

@mehrdad1990
Copy link

@mehrdad1990 The problem is with rootRenderObject, specially the one that is inside the GetPosition class. For what I have learned, after app goes to background, IOS will trigger several builds, to make image snapshots of the app in the launcher (probably for landscape and portrait modes, and also dark and light themes) In that builds anything accessing context gets rebuilt. Checking State.mounted in that situation usually returns true, but the context is marked as detached, as seen by context.toString. When findRenderObject is called with such a context, _lifecycleState returns inactive, and therefore the framework throws the error.

Conditionally removing the ShowCaseWidget is not the best solution, since the user can send the app to back before the showcase has ended, and that would anyway trigger the error.

I have created an issue in Flutter just to check if the IOS build calls are normal.

But for sure Showcase and ShowCaseWidget should be able to build in that situation, as every other widget does.

Regards

Does this PR fix the issue? this is a try-catch wrap. check if it fixes the problem but I don't think this is the best.

@pamafe1976
Copy link
Author

pamafe1976 commented Jun 21, 2024

Hi @mehrdad1990 ,
Unfortunately I cant test the PR right now, but I'm pretty sure the error will still be thrown as long as findRenderObject is called.
What ended up working for me was this replacement for AnchoredOverlay. It only assigns the box the first time, to prevent it from calling findRenderObject when the app goes to background.
Its not nice, I know, but it was the only option I found with my limited knowledge of the package.
Maybe it helps someone with more insight to find out how to solve it.

Regards

class AnchoredOverlay extends StatefulWidget {
  final bool showOverlay;
  final OverlayBuilderCallback? overlayBuilder;
  final Widget? child;
  final RenderObject? rootRenderObject;

  const AnchoredOverlay({
    super.key,
    this.showOverlay = false,
    this.overlayBuilder,
    this.child,
    this.rootRenderObject,
  });

  @override
  State<AnchoredOverlay> createState() => _AnchoredOverlay();
}

class _AnchoredOverlay extends State<AnchoredOverlay> {
  RenderBox? box;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        return (mounted)
            ? OverlayBuilder(
                showOverlay: widget.showOverlay,
                overlayBuilder: (overlayContext) {
                  // To calculate the "anchor" point we grab the render box of
                  // our parent Container and then we find the center of that box.
                  box ??= context.findRenderObject() as RenderBox;
                  if (box!.attached == false) {
                    return SizedBox(child: widget.child);
                  }
                  final topLeft = box!.size.topLeft(
                    box!.localToGlobal(
                      const Offset(0.0, 0.0),
                      ancestor: widget.rootRenderObject,
                    ),
                  );
                  final bottomRight = box!.size.bottomRight(
                    box!.localToGlobal(
                      const Offset(0.0, 0.0),
                      ancestor: widget.rootRenderObject,
                    ),
                  );
                  Rect anchorBounds;
                  anchorBounds = (topLeft.dx.isNaN ||
                          topLeft.dy.isNaN ||
                          bottomRight.dx.isNaN ||
                          bottomRight.dy.isNaN)
                      ? const Rect.fromLTRB(0.0, 0.0, 0.0, 0.0)
                      : Rect.fromLTRB(
                          topLeft.dx,
                          topLeft.dy,
                          bottomRight.dx,
                          bottomRight.dy,
                        );
                  final anchorCenter = box!.size.center(topLeft);
                  return widget.overlayBuilder!(
                      overlayContext, anchorBounds, anchorCenter);
                },
                child: widget.child,
              )
            : SizedBox(child: widget.child);
      },
    );
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants