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.
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:
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. _