跪拜 Guibai
← Back to the summary

Upgrading flutter_easyloading to 4.x Leaks MaterialApp's Error TextStyle into Cupertino Popups

It started out fine.

Suddenly it went wild.

I never expected this, never expected this.

1. The Phenomenon

The interface style suddenly changed.

Abnormal

The font became larger, and any text without a specified color turned bright red. All Text widgets had yellow double underlines.

This code hadn't been changed. The normal situation should be:

right_pitch

This is the desired effect.

2. Conclusion

flutter_easyloading: ^4.0.2

Upgrading caused the problem. The previous version 3.0.5 was released on 2022.05.23, and version 4.X was finally released. The changelog didn't mention any side effects. I never expected it would affect the styling of showCupertinoModalPopup().

Solution

When initializing EasyLoading, modify it to:

builder: EasyLoading.init(
  builder: (context, child) {
    return DefaultTextStyle(style: Theme.of(context).textTheme.bodyMedium!, child: child!);
  },
)

I searched for a long time without finding the cause; it was AI that found it for me. In the future, I should be a tester, not a developer.

3. The Cause

The initialization of EasyLoading is the same.

MaterialApp(
    initialRoute: "/",
    builder: EasyLoading.init(),
)

1. Old Version

In version 3.0.5 of EasyLoading:

@override
  Widget build(BuildContext context) {
    return Material(
      child: Overlay(
        initialEntries: [
          EasyLoadingOverlayEntry(
            builder: (BuildContext context) {
              if (widget.child != null) {
                return widget.child!;
              } else {
                return Container();
              }
            },
          ),
          _overlayEntry,
        ],
      ),
    );
  }

This Material provides the normal Material default text style to the root Navigator/Overlay below.

So when using showCupertinoModalPopup, it uses the Material set by EasyLoading.

2. New Version

In version 4.X of EasyLoading:

@override
  Widget build(BuildContext context) {
    _syncOverlay();
    final session = easyLoadingRuntime.session;
    return Stack(
      fit: StackFit.passthrough,
      clipBehavior: Clip.none,
      children: <Widget>[
        Material(
          type: MaterialType.transparency,
          textStyle: DefaultTextStyle.of(context).style,
          child: widget.child ?? const SizedBox.shrink(),
        ),
        if (session != null)
          Positioned.fill(
            child: _EasyLoadingOverlay(
              key: _overlayKey,
              session: session,
              onPresented: () => easyLoadingRuntime.didPresent(session.id),
              onDismiss: () {
                unawaited(
                  easyLoadingRuntime.dismiss(
                    reason: EasyLoadingDismissReason.tap,
                  ),
                );
              },
            ),
          ),
      ],
    );
  }

The textStyle is set to DefaultTextStyle.of(context).style, which is the style in MaterialApp.

SO showCupertinoModalPopup uses the Material set by EasyLoading.

3. The Cause

After executing showCupertinoModalPopup, the layer structure is:

MaterialApp
└─ EasyLoading wrapper layer
   └─ Root Navigator
      └─ Overlay
         ├─ Original page Route
         │  └─ Scaffold
         │     └─ Normal Material text environment
         │
         └─ CupertinoModalPopupRoute
            └─ CouponPackageDetail

CouponPackageDetail is the view in the popup, and it does not set a Material. SO it inherits the Material from the EasyLoading wrapper layer above the root Navigator.

Old Version

No textStyle was specified, so it used the default Theme.of(context).textTheme.bodyMedium.

New Version

The textStyle is specified as DefaultTextStyle.of(context).style. This DefaultTextStyle gets the textStyle from MaterialApp:

Widget _buildWidgetApp(BuildContext context) {
    final Color materialColor = widget.color ?? widget.theme?.primaryColor ?? Colors.blue;
    if (_usesRouter) {
      return WidgetsApp.router(
        key: GlobalObjectKey(this),
        routeInformationProvider: widget.routeInformationProvider,
        routeInformationParser: widget.routeInformationParser,
        routerDelegate: widget.routerDelegate,
        routerConfig: widget.routerConfig,
        backButtonDispatcher: widget.backButtonDispatcher,
        onNavigationNotification: widget.onNavigationNotification,
        builder: _materialBuilder,
        title: widget.title,
        onGenerateTitle: widget.onGenerateTitle,
        textStyle: _errorTextStyle,
        color: materialColor,
        locale: widget.locale,
        localizationsDelegates: _localizationsDelegates,
        localeResolutionCallback: widget.localeResolutionCallback,
        localeListResolutionCallback: widget.localeListResolutionCallback,
        supportedLocales: widget.supportedLocales,
        showPerformanceOverlay: widget.showPerformanceOverlay,
        showSemanticsDebugger: widget.showSemanticsDebugger,
        debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
        exitWidgetSelectionButtonBuilder: _exitWidgetSelectionButtonBuilder,
        moveExitWidgetSelectionButtonBuilder: _moveExitWidgetSelectionButtonBuilder,
        tapBehaviorButtonBuilder: _tapBehaviorButtonBuilder,
        shortcuts: widget.shortcuts,
        actions: widget.actions,
        restorationScopeId: widget.restorationScopeId,
      );
    }

In _MaterialAppState, the build is set to _errorTextStyle:

const TextStyle _errorTextStyle = TextStyle(
  color: Color(0xD0FF0000),
  fontFamily: 'monospace',
  fontSize: 48.0,
  fontWeight: FontWeight.w900,
  decoration: TextDecoration.underline,
  decorationColor: Color(0xFFFFFF00),
  decorationStyle: TextDecorationStyle.double,
  debugLabel: 'fallback style; consider putting your text in a Material',
);

Yes, that's it. That's what you see.

If a popup, etc.``

4. Overlay

Overlay is a container in Flutter specifically used to "stack and display UI". It allows new Widgets to overlay on top of the current page without replacing the current page.

There are many Routes in the Overlay of Navigator:

Navigator
└─ Overlay
   ├─ Route A
   ├─ Route B
   ├─ Dialog Route
   ├─ Popup Route
   └─ Other floating layers

When executing showCupertinoModalPopup:

MaterialApp
│
└─ EasyLoading
   │
   └─ Navigator
      │
      └─ Overlay
         │
         ├─ Original page Route
         │  └─ Scaffold
         │     └─ Page content
         │
         ├─ ModalBarrier
         │  └─ Semi-transparent mask
         │
         └─ CupertinoModalPopupRoute
            └─ CouponPackageDetail

CouponPackageDetail is not a child of the page where the popup is located, but creates a route: CupertinoModalPopupRoute, so it does not inherit the Material of the original page.

The End

In the AI era, there is no problem AI cannot solve. If it can't, please upgrade your plan. _