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

InheritedWidget depending on the result of "MediaQuery.of(context)" does not work after navigation #13484

Closed
hundeva opened this issue Dec 11, 2017 · 8 comments

Comments

@hundeva
Copy link

hundeva commented Dec 11, 2017

So I have a custom InheritedWidget that is depending on the result of MediaQuery.of(context) (basically, for dynamic dimensions depending on device width/height, would be very useful for rotation), which works perfectly, up until I navigate to a different route, after that, the of(context) mechanism returns null. Please see the example code below, tap on "Tap to navigate" button, that would replace the currently visible ConsumerWidget with a new instance of ConsumerWidget, and observe the crash.

I don't really understand why this is happening, if I had to guess, an InheritedWidget depending on another InheritedWidget doesn't seem to keep working after a navigation event. Am I doing something wrong? Is there an alternate way?

Steps to Reproduce

import 'package:flutter/material.dart';

void main() {
  runApp(new TestApp());
}

class TestApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new Builder(
        builder: (context) {
          return new MyInheritedWidget(
            data: MediaQuery.of(context),
            child: new ConsumerWidget(),
          );
        },
      ),
    );
  }
}

class MyInheritedWidget extends InheritedWidget {

  static MyInheritedWidget of(BuildContext context) =>
      context.inheritFromWidgetOfExactType(MyInheritedWidget);

  final MediaQueryData _data;

  MyInheritedWidget({
    Key key,
    MediaQueryData data,
    Widget child,
  })
      :
        _data = data,
        super(key: key, child: child);

  @override
  bool updateShouldNotify(MyInheritedWidget oldWidget) =>
      _data != oldWidget._data;

  String get deviceWidth => _data.size.width.toString();
}

class ConsumerWidget extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Device width is ${MyInheritedWidget
            .of(context)
            .deviceWidth}'),
      ),

      body: new Center(
        child: new FlatButton(
          child: new Text('Tap to navigate'),
          onPressed: () => _navigate(context),
        ),
      ),
    );
  }

  void _navigate(BuildContext context) {
    Navigator.of(context)
      ..pop()
      ..push(
        new MaterialPageRoute(
          builder: (_) => new ConsumerWidget(),
        ),
      );
  }
}

Screenshot right after tapping on navigation button in example code.
The exception log:

Logs

I/flutter ( 5389): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter ( 5389): The following NoSuchMethodError was thrown building ConsumerWidget(dirty):
I/flutter ( 5389): The getter 'deviceWidth' was called on null.
I/flutter ( 5389): Receiver: null
I/flutter ( 5389): Tried calling: deviceWidth
I/flutter ( 5389): 
I/flutter ( 5389): When the exception was thrown, this was the stack:
I/flutter ( 5389): #0      Object.noSuchMethod (dart:core-patch/dart:core/object_patch.dart:46)
I/flutter ( 5389): #1      ConsumerWidget.build (file:///C:/Git/commuter/lib/main.dart:54)
I/flutter ( 5389): #2      StatelessElement.build (package:flutter/src/widgets/framework.dart:3599)
I/flutter ( 5389): #3      ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3544)
I/flutter ( 5389): #4      Element.rebuild (package:flutter/src/widgets/framework.dart:3445)
I/flutter ( 5389): #5      ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:3524)
I/flutter ( 5389): #6      ComponentElement.mount (package:flutter/src/widgets/framework.dart:3519)
I/flutter ( 5389): #7      Element.inflateWidget (package:flutter/src/widgets/framework.dart:2857)
I/flutter ( 5389): #8      Element.updateChild (package:flutter/src/widgets/framework.dart:2660)
I/flutter ( 5389): #9      ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3556)
I/flutter ( 5389): #10     Element.rebuild (package:flutter/src/widgets/framework.dart:3445)
I/flutter ( 5389): #11     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:3524)
I/flutter ( 5389): #12     ComponentElement.mount (package:flutter/src/widgets/framework.dart:3519)
I/flutter ( 5389): #13     Element.inflateWidget (package:flutter/src/widgets/framework.dart:2857)
I/flutter ( 5389): #14     Element.updateChild (package:flutter/src/widgets/framework.dart:2660)
I/flutter ( 5389): #15     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3556)
I/flutter ( 5389): #16     Element.rebuild (package:flutter/src/widgets/framework.dart:3445)
I/flutter ( 5389): #17     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:3524)
I/flutter ( 5389): #18     ComponentElement.mount (package:flutter/src/widgets/framework.dart:3519)
I/flutter ( 5389): #19     Element.inflateWidget (package:flutter/src/widgets/framework.dart:2857)
I/flutter ( 5389): #20     Element.updateChild (package:flutter/src/widgets/framework.dart:2660)
I/flutter ( 5389): #21     SingleChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:4532)
I/flutter ( 5389): #22     Element.inflateWidget (package:flutter/src/widgets/framework.dart:2857)
I/flutter ( 5389): #23     Element.updateChild (package:flutter/src/widgets/framework.dart:2660)
I/flutter ( 5389): #24     SingleChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:4532)
I/flutter ( 5389): #25     Element.inflateWidget (package:flutter/src/widgets/framework.dart:2857)
I/flutter ( 5389): #26     Element.updateChild (package:flutter/src/widgets/framework.dart:2660)
I/flutter ( 5389): #27     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3556)
I/flutter ( 5389): #28     Element.rebuild (package:flutter/src/widgets/framework.dart:3445)
I/flutter ( 5389): #29     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:3524)
I/flutter ( 5389): #30     StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:3662)
I/flutter ( 5389): #31     ComponentElement.mount (package:flutter/src/widgets/framework.dart:3519)
I/flutter ( 5389): #32     Element.inflateWidget (package:flutter/src/widgets/framework.dart:2857)
I/flutter ( 5389): #33     Element.updateChild (package:flutter/src/widgets/framework.dart:2660)
I/flutter ( 5389): #34     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3556)
I/flutter ( 5389): #35     Element.rebuild (package:flutter/src/widgets/framework.dart:3445)
I/flutter ( 5389): #36     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:3524)
I/flutter ( 5389): #37     ComponentElement.mount (package:flutter/src/widgets/framework.dart:3519)
I/flutter ( 5389): #38     Element.inflateWidget (package:flutter/src/widgets/framework.dart:2857)
I/flutter ( 5389): #39     Element.updateChild (package:flutter/src/widgets/framework.dart:2660)
I/flutter ( 5389): #40     SingleChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:4532)
I/flutter ( 5389): #41     Element.inflateWidget (package:flutter/src/widgets/framework.dart:2857)
I/flutter ( 5389): #42     Element.updateChild (package:flutter/src/widgets/framework.dart:2660)
I/flutter ( 5389): #43     SingleChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:4532)
I/flutter ( 5389): #44     Element.inflateWidget (package:flutter/src/widgets/framework.dart:2857)
I/flutter ( 5389): #45     Element.updateChild (package:flutter/src/widgets/framework.dart:2660)
I/flutter ( 5389): #46     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3556)
I/flutter ( 5389): #47     Element.rebuild (package:flutter/src/widgets/framework.dart:3445)
I/flutter ( 5389): #48     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:3524)
I/flutter ( 5389): #49     ComponentElement.mount (package:flutter/src/widgets/framework.dart:3519)
I/flutter ( 5389): #50     Element.inflateWidget (package:flutter/src/widgets/framework.dart:2857)
I/flutter ( 5389): #51     Element.updateChild (package:flutter/src/widgets/framework.dart:2660)
I/flutter ( 5389): #52     SingleChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:4532)
I/flutter ( 5389): #53     Element.inflateWidget (package:flutter/src/widgets/framework.dart:2857)
I/flutter ( 5389): #54     Element.updateChild (package:flutter/src/widgets/framework.dart:2660)
I/flutter ( 5389): #55     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3556)
I/flutter ( 5389): #56     Element.rebuild (package:flutter/src/widgets/framework.dart:3445)
I/flutter ( 5389): #57     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:3524)
I/flutter ( 5389): #58     StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:3662)
I/flutter ( 5389): #59     ComponentElement.mount (package:flutter/src/widgets/framework.dart:3519)
I/flutter ( 5389): #60     Element.inflateWidget (package:flutter/src/widgets/framework.dart:2857)
I/flutter ( 5389): #61     Element.updateChild (package:flutter/src/widgets/framework.dart:2660)
I/flutter ( 5389): #62     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3556)
I/flutter ( 5389): #63     Element.rebuild (package:flutter/src/widgets/framework.dart:3445)
I/flutter ( 5389): #64     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:3524)
I/flutter ( 5389): #65     StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:3662)
I/flutter ( 5389): #66     ComponentElement.mount (package:flutter/src/widgets/framework.dart:3519)
I/flutter ( 5389): #67     Element.inflateWidget (package:flutter/src/widgets/framework.dart:2857)
I/flutter ( 5389): #68     Element.updateChild (package:flutter/src/widgets/framework.dart:2660)
I/flutter ( 5389): #69     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3556)
I/flutter ( 5389): #70     Element.rebuild (package:flutter/src/widgets/framework.dart:3445)
I/flutter ( 5389): #71     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:3524)
I/flutter ( 5389): #72     StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:3662)
I/flutter ( 5389): #73     ComponentElement.mount (package:flutter/src/widgets/framework.dart:3519)
I/flutter ( 5389): #74     Element.inflateWidget (package:flutter/src/widgets/framework.dart:2857)
I/flutter ( 5389): #75     Element.updateChild (package:flutter/src/widgets/framework.dart:2660)
I/flutter ( 5389): #76     RenderObjectElement.updateChildren (package:flutter/src/widgets/framework.dart:4319)
I/flutter ( 5389): #77     MultiChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:4647)
I/flutter ( 5389): #78     Element.updateChild (package:flutter/src/widgets/framework.dart:2649)
I/flutter ( 5389): #79     _TheatreElement.update (package:flutter/src/widgets/overlay.dart:507)
I/flutter ( 5389): #80     Element.updateChild (package:flutter/src/widgets/framework.dart:2649)
I/flutter ( 5389): #81     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3556)
I/flutter ( 5389): #82     Element.rebuild (package:flutter/src/widgets/framework.dart:3445)
I/flutter ( 5389): #83     StatefulElement.update (package:flutter/src/widgets/framework.dart:3681)
I/flutter ( 5389): #84     Element.updateChild (package:flutter/src/widgets/framework.dart:2649)
I/flutter ( 5389): #85     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3556)
I/flutter ( 5389): #86     Element.rebuild (package:flutter/src/widgets/framework.dart:3445)
I/flutter ( 5389): #87     ProxyElement.update (package:flutter/src/widgets/framework.dart:3791)
I/flutter ( 5389): #88     Element.updateChild (package:flutter/src/widgets/framework.dart:2649)
I/flutter ( 5389): #89     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:4539)
I/flutter ( 5389): #90     Element.updateChild (package:flutter/src/widgets/framework.dart:2649)
I/flutter ( 5389): #91     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3556)
I/flutter ( 5389): #92     Element.rebuild (package:flutter/src/widgets/framework.dart:3445)
I/flutter ( 5389): #93     StatefulElement.update (package:flutter/src/widgets/framework.dart:3681)
I/flutter ( 5389): #94     Element.updateChild (package:flutter/src/widgets/framework.dart:2649)
I/flutter ( 5389): #95     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:4539)
I/flutter ( 5389): #96     Element.updateChild (package:flutter/src/widgets/framework.dart:2649)
I/flutter ( 5389): #97     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:4539)
I/flutter ( 5389): #98     Element.updateChild (package:flutter/src/widgets/framework.dart:2649)
I/flutter ( 5389): #99     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3556)
I/flutter ( 5389): #100    Element.rebuild (package:flutter/src/widgets/framework.dart:3445)
I/flutter ( 5389): #101    BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2193)
I/flutter ( 5389): #102    BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&RendererBinding&WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:611)
I/flutter ( 5389): #103    BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:203)
I/flutter ( 5389): #104    BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:916)
I/flutter ( 5389): #105    BindingBase&GestureBinding&ServicesBinding&SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:856)
I/flutter ( 5389): #106    BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:768)
I/flutter ( 5389): #107    _invoke (file:///b/build/slave/Linux_Engine/build/src/flutter/lib/ui/hooks.dart:113)
I/flutter ( 5389): #108    _drawFrame (file:///b/build/slave/Linux_Engine/build/src/flutter/lib/ui/hooks.dart:102)
I/flutter ( 5389): ════════════════════════════════════════════════════════════════════════════════════════════════════
I/flutter ( 5389): Another exception was thrown: NoSuchMethodError: The getter 'deviceWidth' was called on null.

This is happening both on android emulators and iOS simulators.

Flutter Doctor

[√] Flutter (on Microsoft Windows [Version 10.0.16299.64], locale en-US, channel master)
    • Flutter at C:\flutter
    • Framework revision 0c09179b65 (2 days ago), 2017-12-09 22:04:06 -0800
    • Engine revision edb0201fa2
    • Tools Dart version 1.25.0-dev.11.0
    • Engine Dart version 2.0.0-edge.a38ac7cf127f4611c873c2f2d523c06ce06b1342

[√] Android toolchain - develop for Android devices (Android SDK 27.0.0)
    • Android SDK at C:\android-sdk
    • Android NDK location not configured (optional; useful for native profiling support)
    • Platform android-27, build-tools 27.0.0
    • ANDROID_HOME = C:\android-sdk
    • Java binary at: C:\android-studio\jre\bin\java
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-915-b01)
[√] Android Studio (version 3.0)
    • Android Studio at C:\android-studio
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-915-b01)
[√] Connected devices
    • Android SDK built for x86 • emulator-5554 • android-x86 • Android 8.0.0 (API 26) (emulator)
@Hixie
Copy link
Contributor

Hixie commented Dec 11, 2017 via email

@hundeva
Copy link
Author

hundeva commented Dec 11, 2017

Ahhhhhhhhhhh I understand it now. The thing that confused me a lot is, I used a similar approach for theming colors, but the outer most widget was the theming inherited widget, "outside of the "/" route", and the MaterialApp was "inside" it. Thus, when chaning routes, the theming inherited widget was still there.

So yeah, the solution is, to move the widget depending on MediaQuery outside of the MaterialApp, but than it won't work, because it needs a MaterialApp... Guess I have to rethink my approach.

Thank you, that helped!

@hundeva hundeva closed this as completed Dec 11, 2017
@hundeva
Copy link
Author

hundeva commented Dec 11, 2017

Just to drop a solution:

import 'dart:math';

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

class AppDimensions extends InheritedWidget {

  static AppDimensions of(BuildContext context) =>
      context.inheritFromWidgetOfExactType(AppDimensions);

  AppDimensions({
    Key key,
    @required Widget child,
  }) : super(key: key, child: child);

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) => false;

  double _fix(double value) => value;

  MediaQueryData _data(BuildContext context) => MediaQuery.of(context);

  static const _cardMinimumWidth = 296.0;

  int cardCount(BuildContext context) =>
      max(_data(context).size.width ~/ _cardMinimumWidth, 1);

  double get paddingSmall => _fix(4.0);

  double get paddingMedium => _fix(8.0);

  double get paddingLarge => _fix(16.0);

}

This way, I can access dimensions similar as I would access themed colors. The padding values don't need MediaQuery, but the cardCount value does need it. Instead of depending on it early, I'm passing the BuildContext as late as possible, and thus depending on it as late as possible.

Not the most elegant solution, but for a first iteration, everything seems to work.

@Hixie
Copy link
Contributor

Hixie commented Dec 11, 2017

Yeah, that makes sense. You could also just have your .of method do all that work for you, and return the card count.

@hundeva
Copy link
Author

hundeva commented Dec 12, 2017

I wasn't really satisfied with the solution I came up with, so I decided to dig deeper. I think I came up with something that is much better, and everything works as I expect it should work. However, I'm not sure how future proof I am, some insight would be appreciated.

What I did was, search for when MediaQuery is first introduced (shout out for the helpful, albeit weird-at-first-sight widget tree dump), this was what I was looking for, in WidgetsApp:

    return new MediaQuery(
      data: new MediaQueryData.fromWindow(ui.window),
      child: new Localizations(
        locale: widget.locale ?? _locale,
        delegates: _localizationsDelegates.toList(),
        // This Builder exists to provide a context below the Localizations widget.
        // The onGenerateCallback() can refer to Localizations via its context
        // parameter.
        child: new Builder(
          builder: (BuildContext context) {
            String title = widget.title;
            if (widget.onGenerateTitle != null) {
              title = widget.onGenerateTitle(context);
              assert(title != null, 'onGenerateTitle must return a non-null String');
            }
            return new Title(
              title: title,
              color: widget.color,
              child: result,
            );
          },
        ),
      ),
    );

So, after that, I wrapped my AppDimensions InheritedWidget the following way:

        child: new MediaQuery(
          data: new MediaQueryData.fromWindow(ui.window),
          child: new Builder(
            builder: (context) {
              return new AppDimensions(
                data: MediaQuery.of(context),
                child: new Builder(
                  builder: (
                      context) {
                    return new MaterialApp(
                      title: _title,
                      theme: _theme,
                      home: new HomeScreen(),
                    );
                  },
                ),
              );
            },
          ),
        ),

To watch for the changes, I have set up my own WidgetsBindingObserver the following way:

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }


  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

I handle the events the following way:

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {}

  @override
  void didChangeLocale(Locale locale) {}

  @override
  void didChangeMetrics() => setState(() {});

  @override
  void didChangeTextScaleFactor() => setState(() {});

  @override
  void didHaveMemoryPressure() {}

  @override
  Future<bool> didPopRoute() => new Future.value(false);

  @override
  Future<bool> didPushRoute(String route) => new Future.value(false);

Except the metrics and text scale factor, I don't care about any event, so I won't force a rebuild for them, someone else will anyway.

So, basically I have created my own MediaQuery outside of any WidgetsApp context, and depending on that one. Than, I have created my own WidgetsBindingObserver, to rebuild on events relevant to "me". Thus, when I change routes, it is still there and accessible, and instead of invoking AppDimensions.of(context).cardCount(context), I can once again use properties, and invoke AppDimensions.of(context).cardCount, which is exactly what I am looking for.

The only question I have is, am I doing something wrong, or not future proof, or everything seems to be in order, and I am safe to commit (at least for now) to this solution?

@Hixie
Copy link
Contributor

Hixie commented Dec 13, 2017

It's certainly more work than we'd like you to need to do, but I believe it's using the API as intended.

@archiewx
Copy link

Just to drop a solution:

import 'dart:math';

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

class AppDimensions extends InheritedWidget {

  static AppDimensions of(BuildContext context) =>
      context.inheritFromWidgetOfExactType(AppDimensions);

  AppDimensions({
    Key key,
    @required Widget child,
  }) : super(key: key, child: child);

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) => false;

  double _fix(double value) => value;

  MediaQueryData _data(BuildContext context) => MediaQuery.of(context);

  static const _cardMinimumWidth = 296.0;

  int cardCount(BuildContext context) =>
      max(_data(context).size.width ~/ _cardMinimumWidth, 1);

  double get paddingSmall => _fix(4.0);

  double get paddingMedium => _fix(8.0);

  double get paddingLarge => _fix(16.0);

}

This way, I can access dimensions similar as I would access themed colors. The padding values don't need MediaQuery, but the cardCount value does need it. Instead of depending on it early, I'm passing the BuildContext as late as possible, and thus depending on it as late as possible.

Not the most elegant solution, but for a first iteration, everything seems to work.

MediaQueryData get _mediaQuery => MediaQuery.of(context); // in stateful class

@github-actions
Copy link

github-actions bot commented Sep 1, 2021

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 1, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants