provider icon indicating copy to clipboard operation
provider copied to clipboard

`ProxyProvider` triggers an assertion failure when depending on a provided value of the same type it is providing

Open jjoelson opened this issue 1 year ago • 1 comments

Hi and thank you for this great package. Please let me know if there's any additional detail I can add to this issue!

Describe the bug When using ProxyProvider (or ProxyProvider0, ProxyProvider2, etc.) to provide a value of a given type, while also depending an an ancestor value of that same type, a widget rebuild triggers an assertion failure within the Flutter framework:

The following assertion was thrown building _InheritedProviderScope<AppTheme?>(dirty, dependencies: [_InheritedProviderScope<AppTheme?>], value: Instance of 'PartialAppTheme'):
'package:flutter/src/widgets/framework.dart': Failed assertion: line 5472 pos 14: '() {
package:flutter/…/widgets/framework.dart:5472
        // check that it really is our descendant
        Element? ancestor = dependent._parent;
        while (ancestor != this && ancestor != null) {
          ancestor = ancestor._parent;
        }
        return ancestor == this;
      }()': is not true.


The relevant error-causing widget was
ProxyProvider0<AppTheme>

To Reproduce

A full reproduction is in the collapsed section below, but the PartialAppThemeProvider here is the main part:

class AppTheme {
  final String property1 = 'AppThemeProperty1';
  final String property2 = 'AppThemeProperty2';
}

class PartialAppTheme implements AppTheme {
  final AppTheme baseTheme;

  @override
  final String property1;

  @override
  String get property2 => baseTheme.property2;

  PartialAppTheme(this.baseTheme, {required this.property1});
}

class PartialAppThemeProvider extends SingleChildStatelessWidget {
  const PartialAppThemeProvider({
    super.key,
    super.child,
    required this.property1,
  });

  final String property1;

  @override
  Widget buildWithChild(BuildContext context, Widget? child) {
    return ProxyProvider0<AppTheme>(
      update: (context, _) {
        final baseTheme = Provider.of<AppTheme>(context);

        return PartialAppTheme(
          baseTheme,
          property1: property1,
        );
      },
      child: child,
    );
  }
}

PartialAppThemeProvider attempts to "override" the AppTheme provided by an ancestor based on that ancestor value and a widget parameter.

Full reproduction code and assertion failure stack trace
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider/single_child_widget.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return Provider<AppTheme>(
      create: (context) => AppTheme(),
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const HomePage(),
      ),
    );
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  late bool _themeChanged = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Column(
        children: [
          const ThemedWidget(),
          const Divider(),
          PartialAppThemeProvider(
            property1:
                _themeChanged ? 'PartialProperty1' : 'ChangedPartialProperty1',
            child: const ThemedWidget(),
          ),
          const Divider(),
          TextButton(
            onPressed: () {
              setState(() {
                _themeChanged = !_themeChanged;
              });
            },
            child: Text('Change Theme'),
          ),
        ],
      ),
    );
  }
}

class ThemedWidget extends StatelessWidget {
  const ThemedWidget({super.key});

  @override
  Widget build(BuildContext context) {
    final theme = context.watch<AppTheme>();

    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(
        children: [
          Text('property1: ${theme.property1}'),
          Text('property2: ${theme.property2}'),
        ],
      ),
    );
  }
}

class PartialAppThemeProvider extends SingleChildStatelessWidget {
  const PartialAppThemeProvider({
    super.key,
    super.child,
    required this.property1,
  });

  final String property1;

  @override
  Widget buildWithChild(BuildContext context, Widget? child) {
    return ProxyProvider0<AppTheme>(
      update: (context, _) {
        final baseTheme = Provider.of<AppTheme>(context);

        return PartialAppTheme(
          baseTheme,
          property1: property1,
        );
      },
      child: child,
    );
  }
}

class AppTheme {
  final String property1 = 'AppThemeProperty1';
  final String property2 = 'AppThemeProperty2';
}

class PartialAppTheme implements AppTheme {
  final AppTheme baseTheme;

  @override
  final String property1;

  @override
  String get property2 => baseTheme.property2;

  PartialAppTheme(this.baseTheme, {required this.property1});
}

Assertion failure stack trace:

═══════ Exception caught by widgets library ═══════════════════════════════════
The following assertion was thrown building _InheritedProviderScope<AppTheme?>(dirty, dependencies: [_InheritedProviderScope<AppTheme?>], value: Instance of 'PartialAppTheme'):
'package:flutter/src/widgets/framework.dart': Failed assertion: line 5472 pos 14: '() {
package:flutter/…/widgets/framework.dart:5472
        // check that it really is our descendant
        Element? ancestor = dependent._parent;
        while (ancestor != this && ancestor != null) {
          ancestor = ancestor._parent;
        }
        return ancestor == this;
      }()': is not true.


The relevant error-causing widget was
ProxyProvider0<AppTheme>
lib/main.dart:95
When the exception was thrown, this was the stack
#2      InheritedElement.notifyClients
package:flutter/…/widgets/framework.dart:5472
#3      _InheritedProviderScopeElement.build
package:provider/src/inherited_provider.dart:552
#4      ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4878
#5      Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#6      ProxyElement.update
package:flutter/…/widgets/framework.dart:5228
#7      _InheritedProviderScopeElement.update
package:provider/src/inherited_provider.dart:523
#8      Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#9      ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#10     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#11     StatelessElement.update
package:flutter/…/widgets/framework.dart:4956
#12     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#13     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#14     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#15     StatelessElement.update
package:flutter/…/widgets/framework.dart:4956
#16     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#17     RenderObjectElement.updateChildren
package:flutter/…/widgets/framework.dart:5904
#18     MultiChildRenderObjectElement.update
package:flutter/…/widgets/framework.dart:6460
#19     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#20     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#21     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#22     StatelessElement.update
package:flutter/…/widgets/framework.dart:4956
#23     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#24     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#25     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#26     StatelessElement.update
package:flutter/…/widgets/framework.dart:4956
#27     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#28     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#29     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#30     ProxyElement.update
package:flutter/…/widgets/framework.dart:5228
#31     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#32     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#33     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#34     ProxyElement.update
package:flutter/…/widgets/framework.dart:5228
#35     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#36     RenderObjectElement.updateChildren
package:flutter/…/widgets/framework.dart:5904
#37     MultiChildRenderObjectElement.update
package:flutter/…/widgets/framework.dart:6460
#38     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#39     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#40     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#41     ProxyElement.update
package:flutter/…/widgets/framework.dart:5228
#42     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#43     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#44     StatefulElement.performRebuild
package:flutter/…/widgets/framework.dart:5050
#45     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#46     StatefulElement.update
package:flutter/…/widgets/framework.dart:5082
#47     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#48     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#49     StatefulElement.performRebuild
package:flutter/…/widgets/framework.dart:5050
#50     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#51     StatefulElement.update
package:flutter/…/widgets/framework.dart:5082
#52     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#53     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#54     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#55     ProxyElement.update
package:flutter/…/widgets/framework.dart:5228
#56     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#57     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#58     StatefulElement.performRebuild
package:flutter/…/widgets/framework.dart:5050
#59     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#60     StatefulElement.update
package:flutter/…/widgets/framework.dart:5082
#61     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#62     SingleChildRenderObjectElement.update
package:flutter/…/widgets/framework.dart:6307
#63     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#64     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#65     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#66     ProxyElement.update
package:flutter/…/widgets/framework.dart:5228
#67     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#68     SingleChildRenderObjectElement.update
package:flutter/…/widgets/framework.dart:6307
#69     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#70     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#71     StatefulElement.performRebuild
package:flutter/…/widgets/framework.dart:5050
#72     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#73     StatefulElement.update
package:flutter/…/widgets/framework.dart:5082
#74     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#75     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#76     StatefulElement.performRebuild
package:flutter/…/widgets/framework.dart:5050
#77     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#78     StatefulElement.update
package:flutter/…/widgets/framework.dart:5082
#79     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#80     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#81     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#82     ProxyElement.update
package:flutter/…/widgets/framework.dart:5228
#83     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#84     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#85     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#86     ProxyElement.update
package:flutter/…/widgets/framework.dart:5228
#87     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#88     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#89         Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#90     ProxyElement.update
package:flutter/…/widgets/framework.dart:5228
#91     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#92     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#93     StatefulElement.performRebuild
package:flutter/…/widgets/framework.dart:5050
#94     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#95     StatefulElement.update
package:flutter/…/widgets/framework.dart:5082
#96     Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#97     ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#98     Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#99     ProxyElement.update
package:flutter/…/widgets/framework.dart:5228
#100    Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#101    ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#102    StatefulElement.performRebuild
package:flutter/…/widgets/framework.dart:5050
#103    Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#104    StatefulElement.update
package:flutter/…/widgets/framework.dart:5082
#105    Element.updateChild
package:flutter/…/widgets/framework.dart:3570
#106    ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4904
#107    StatefulElement.performRebuild
package:flutter/…/widgets/framework.dart:5050
#108    Element.rebuild
package:flutter/…/widgets/framework.dart:4604
#109    BuildOwner.buildScope
package:flutter/…/widgets/framework.dart:2667
#110    WidgetsBinding.drawFrame
package:flutter/…/widgets/binding.dart:882
#111    RendererBinding._handlePersistentFrameCallback
package:flutter/…/rendering/binding.dart:378
#112    SchedulerBinding._invokeFrameCallback
package:flutter/…/scheduler/binding.dart:1175
#113    SchedulerBinding.handleDrawFrame
package:flutter/…/scheduler/binding.dart:1104
#114    SchedulerBinding._handleDrawFrame
package:flutter/…/scheduler/binding.dart:1015
#115    _invoke (dart:ui/hooks.dart:148:13)
#116    PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:318:5)
#117    _drawFrame (dart:ui/hooks.dart:115:31)
(elided 2 frames from class _AssertionError)
════════════════════════════════════════════════════════════════════════════════

Expected behavior My expectation is that I can use ProxyProvider to both depend on and provide a new value for the same type.

I can work around the issue by avoiding ProxyProvider and tracking the ancestor dependency manually by using Provider.of<AppTheme>(context) in a stateful widget:

class PartialAppThemeProvider extends SingleChildStatefulWidget {
  const PartialAppThemeProvider({
    super.key,
    super.child,
    required this.property1,
  });

  final String? property1;

  @override
  State<PartialAppThemeProvider> createState() =>
      _PartialAppThemeProviderState();
}

class _PartialAppThemeProviderState
    extends SingleChildState<PartialAppThemeProvider> {
  late AppTheme _theme;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    _updateTheme();
  }

  @override
  void didUpdateWidget(covariant PartialAppThemeProvider oldWidget) {
    super.didUpdateWidget(oldWidget);

    if (widget.property1 != oldWidget.property1) {
      _updateTheme();
    }
  }

  @override
  Widget buildWithChild(BuildContext context, Widget? child) {
    return Provider<AppTheme>.value(
      value: _theme,
      child: child,
    );
  }

  void _updateTheme() {
    final baseTheme = Provider.of<AppTheme>(context);

    if (widget.property1 != null) {
      setState(() {
        _theme = PartialAppTheme(
          baseTheme,
          property1: widget.property1!,
        );
      });
    } else {
      setState(() {
        _theme = baseTheme;
      });
    }
  }
}

jjoelson avatar Dec 28 '22 22:12 jjoelson

After messing with it a bit more, I found a simpler work around by using the build method's context instead of the update callback's context to get the base theme:

return ProxyProvider0<AppTheme>(
  update: (innerContext, _) {
    // Use outer context instead of the `update` callback's context
    final baseTheme = Provider.of<AppTheme>(context);

    return PartialAppTheme(
      baseTheme,
      property1: property1,
    );
  },
  child: child,
);

(Though perhaps this partially defeats the purpose of proxy provider by making my entire widget rebuild when the base theme changes instead of just re-running the update closure)

Using ProxyProvider to fetch the dependency automatically triggers the assertion failure:

// This triggers the assertion failure on rebuild
return ProxyProvider<AppTheme, AppTheme>(
  update: (context, baseTheme, _) {
    return PartialAppTheme(
      baseTheme,
      property1: property1,
    );
  },
  child: child,
);

jjoelson avatar Dec 29 '22 16:12 jjoelson