Skip to content

Instantly share code, notes, and snippets.

@rydmike
Last active March 30, 2021 08:15
Show Gist options
  • Save rydmike/3a0818223933d459f4ce2760ddd92661 to your computer and use it in GitHub Desktop.
Save rydmike/3a0818223933d459f4ce2760ddd92661 to your computer and use it in GitHub Desktop.
Flutter CanvasKit Random Crash (https://github.com/flutter/flutter/issues/67949)
// MIT License
// Copyright 2020 Mike Rydstrom
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// Const and finals for setup and theme
const double kEdgePadding = 16.0;
const double kMaxContentWidth = 1200.0;
const double kExpandWidth = 250;
const double kShrinkWidth = 70;
const double kMinCardWidth = 200;
const int kMaxCards = 200;
// Custom color scheme
const Color lightPrimary = Color(0xFF264E36); // Eden 23%
const Color lightPrimaryVariant = Color(0xFF224430); // Eden 20%
const Color lightSecondary = Color(0xFF797b3a); // Guacamole 35%
const Color lightSecondaryVariant = Color(0xFF555729); // Guacamole 25%
final Color lightCanvas = Color.alphaBlend(
lightPrimary.withAlpha((256 * 0.07).toInt()), Colors.white);
const Color darkPrimary = Color(0xFF59a678); // Eden light 50%
const Color darkPrimaryVariant = Color(0xFF478560); // Eden light 40%
const Color darkSecondary = Color(0xFFd5d6a8); // Guacamole 75%
const Color darkSecondaryVariant = Color(0xFFbbbe74); // Guacamole 60%
final Color darkCanvas = Color.alphaBlend(
darkPrimary.withAlpha((256 * 0.12).toInt()), const Color(0xFF121212));
const ColorScheme lightScheme = ColorScheme.light(
primary: lightPrimary,
primaryVariant: lightPrimaryVariant,
secondary: lightSecondary,
secondaryVariant: lightSecondaryVariant,
);
const ColorScheme darkScheme = ColorScheme.dark(
primary: darkPrimary,
primaryVariant: darkPrimaryVariant,
secondary: darkSecondary,
secondaryVariant: darkSecondaryVariant,
);
final ThemeData lightTheme = ThemeData.from(colorScheme: lightScheme).copyWith(
canvasColor: lightCanvas,
toggleableActiveColor: lightScheme.secondary,
buttonTheme: const ButtonThemeData(
colorScheme: lightScheme,
textTheme: ButtonTextTheme.primary,
),
);
final ThemeData darkTheme = ThemeData.from(colorScheme: darkScheme).copyWith(
canvasColor: darkCanvas,
toggleableActiveColor: darkScheme.secondary,
buttonTheme: const ButtonThemeData(
colorScheme: darkScheme,
textTheme: ButtonTextTheme.primary,
),
);
// ****************************************************************************
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool isDarkMode;
@override
void initState() {
isDarkMode = false;
super.initState();
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Theme.of(context).brightness,
systemNavigationBarColor: theme.scaffoldBackgroundColor,
systemNavigationBarIconBrightness: theme.brightness == Brightness.light
? Brightness.dark
: Brightness.light,
),
);
return MaterialApp(
title: 'Issue Discovery',
debugShowCheckedModeBanner: false,
theme: lightTheme,
darkTheme: darkTheme,
themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light,
home: DemoPage(
title: 'CanvasKit Crash #67949',
setDarkMode: (bool value) {
setState(() {
isDarkMode = value;
});
},
),
);
}
}
class DemoPage extends StatefulWidget {
const DemoPage({
Key key,
this.title,
this.setDarkMode,
}) : super(key: key);
final String title;
final ValueChanged<bool> setDarkMode;
@override
_DemoPageState createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
double width;
bool isSidePanelExpanded;
bool showSidePanel;
bool isDarkMode;
bool problemOn;
@override
void initState() {
width = kExpandWidth;
isSidePanelExpanded = true;
showSidePanel = true;
isDarkMode = false;
problemOn = true;
super.initState();
}
@override
Widget build(BuildContext context) {
final MediaQueryData media = MediaQuery.of(context);
final double topPadding = media.padding.top;
final double bottomPadding = media.padding.bottom;
final Size size = media.size;
final ThemeData theme = Theme.of(context);
final TextTheme textTheme = theme.textTheme;
final TextStyle headline6 = textTheme.headline6;
final TextStyle caption = theme.primaryTextTheme.caption;
return Row(
children: <Widget>[
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: kExpandWidth),
child: Material(
elevation: 0,
child: AnimatedContainer(
//
// WEIRD BEHAVIOR CAUSING LINE
// This line previously caused on Web master CanvasKit build,
// it was not needed for the use case on the AnimatedContainer
// below.
// Past issue: https://github.com/flutter/flutter/issues/63740
// that this demo was orginally made for.
//
// In this CanvasKit crash issue demo the problematic color
// assignment can be toggled on and/off to see its effect here too.
color: problemOn ? Theme.of(context).canvasColor : null,
duration: const Duration(milliseconds: 300),
width: width,
child: SideContent(
isVisible: showSidePanel,
menuWidth: kExpandWidth,
),
),
),
),
Expanded(
child: Scaffold(
extendBodyBehindAppBar: true,
extendBody: true,
appBar: AppBar(
title: Row(
children: <Widget>[
Text(widget.title),
// Show canvas size in the app bar
Expanded(
child: Text(
" W:${size.width.round()} H:${size.height.round()}",
style: caption,
textAlign: TextAlign.right,
),
),
],
),
centerTitle: false,
elevation: 0,
backgroundColor: Colors.transparent,
// Gradient partially transparent AppBar
flexibleSpace: Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Theme.of(context).dividerColor),
),
gradient: LinearGradient(
begin: AlignmentDirectional.centerStart,
end: AlignmentDirectional.centerEnd,
colors: <Color>[
theme.primaryColor,
theme.primaryColor.withOpacity(0.7),
],
),
),
child: null,
),
),
body: Scrollbar(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: kMaxContentWidth),
child: ListView(
padding: EdgeInsets.fromLTRB(
kEdgePadding,
topPadding + kToolbarHeight + kEdgePadding,
kEdgePadding,
kEdgePadding + bottomPadding,
),
children: <Widget>[
Text(
'CanvasKit Crash Issue',
style: textTheme.headline4,
),
const Divider(),
Text('Issue', style: headline6),
const Text(
'CanvasKit application crashes at random, having a lot of text '
'on the page seems to be key to get it to crash, or so I think '
'so I am putting a lot of text here. Well not a lot, but some at least. '
'We will see if it makes any differences. The next will just '
'be gibberish that I can remove.\n\n'
''
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do '
'eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut '
'porttitor leo a diam sollicitudin tempor id eu nisl. Volutpat diam ut '
'venenatis tellus in. Porttitor leo a diam sollicitudin tempor. R'
'honcus aenean vel elit scelerisque mauris pellentesque pulvinar. Sed '
'facilisis magna etiam. Tempor id eu nisl nunc mi ipsum faucibus vitae. Nisl nisi scelerisque eu '
'ultrices vitae auctor. Et malesuada fames ac turpis. Turpis in '
'eu mi bibendum neque egestas congue quisque egestas.\n\n'
'',
),
const Text(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do '
'eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut '
'porttitor leo a diam sollicitudin tempor id eu nisl. Volutpat diam ut '
'venenatis tellus in. Porttitor leo a diam sollicitudin tempor. R'
'honcus aenean vel elit scelerisque mauris pellentesque pulvinar. Sed '
'ultrices vitae auctor. Et malesuada fames ac turpis. Turpis in '
'eu mi bibendum neque egestas congue quisque egestas.\n\n'
'',
),
const Text(
'Eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut '
'porttitor leo a diam sollicitudin tempor id eu nisl. Volutpat diam ut '
'venenatis tellus in. Porttitor leo a diam sollicitudin tempor. R'
'porttitor leo a diam sollicitudin tempor id eu nisl. Volutpat diam ut '
'venenatis tellus in. Porttitor leo a diam sollicitudin tempor. R'
'honcus aenean vel elit scelerisque mauris pellentesque pulvinar. Sed '
'honcus aenean vel elit scelerisque mauris pellentesque pulvinar. Sed '
'facilisis magna etiam. Tempor id eu nisl nunc mi ipsum faucibus vitae. Nisl nisi scelerisque eu '
'ultrices vitae auctor. Et malesuada fames ac turpis. Turpis in '
'eu mi bibendum neque egestas congue quisque egestas.\n\n'
'',
),
const Text(
'Porttitor leo a diam sollicitudin tempor id eu nisl. Volutpat diam ut '
'venenatis tellus in. Porttitor leo a diam sollicitudin tempor. R'
'honcus aenean vel elit scelerisque mauris pellentesque pulvinar. Sed '
'facilisis magna etiam. Tempor id eu nisl nunc mi ipsum faucibus vitae. Nisl nisi scelerisque eu '
'ultrices vitae auctor. Et malesuada fames ac turpis. Turpis in '
'eu mi bibendum neque egestas congue quisque egestas.\n\n'
'',
),
SwitchListTile.adaptive(
title: const Text(
'Rail or side menu?',
),
subtitle: const Text(
'Turn OFF for a Rail sized side panel.',
),
value: isSidePanelExpanded,
onChanged: (bool value) {
setState(() {
isSidePanelExpanded = value;
if (showSidePanel) {
if (isSidePanelExpanded) {
width = kExpandWidth;
} else {
width = kShrinkWidth;
}
} else {
width = 0;
}
});
},
),
SwitchListTile.adaptive(
title: const Text(
'Show and hide the side panel',
),
subtitle: const Text(
'Turn OFF to hide the side panel completely.'),
value: showSidePanel,
onChanged: (bool value) {
setState(() {
showSidePanel = value;
if (showSidePanel) {
if (isSidePanelExpanded) {
width = kExpandWidth;
} else {
width = kShrinkWidth;
}
} else {
width = 0;
}
});
},
),
//
// Change theme
const Divider(),
Text('Change theme mode', style: headline6),
const Text(
'Theme change animations on the AnimatedContainer '
'used in the side panel are odd, or done twice '
'if the "Make things slower" toggle further below is ON.'),
SwitchListTile.adaptive(
title: const Text(
'Change theme mode',
),
value: isDarkMode,
onChanged: (bool value) {
setState(() {
isDarkMode = value;
widget.setDarkMode(isDarkMode);
});
},
),
const Divider(),
Text('Make things slower or odd', style: headline6),
const Text(
'With this on there will be a theme based color on '
'the AnimatedContainer handling the expanding '
'side panel. This caused severe jank on earlier Flutter '
'versions. It still makes things odd and slower. '),
SwitchListTile.adaptive(
title: const Text(
'Use a "Theme.of" color on the animated container',
),
subtitle: const Text(
'Turn OFF to remove the "Theme.of" color '
'from the AnimatedContainer handling the side '
'panel width.',
),
value: problemOn,
onChanged: (bool value) {
setState(() {
problemOn = value;
});
},
),
//
// Source code location info
const Divider(),
Text('Source code location of this demo',
style: headline6),
const SelectableText(
'https://gist.github.com/rydmike/3a0818223933d459f4ce2760ddd92661'),
const Divider(),
//
// A Card GridView with responsive column amount. This is
// just here to put some content on the page to stress it
// a bit during resizing and scaling.
const SizedBox(height: kEdgePadding),
Text('Colorful cards in a responsive GridView',
style: headline6),
LayoutBuilder(
builder:
(BuildContext context, BoxConstraints constraint) {
final int nrOfColumns =
(constraint.maxWidth.toInt()) ~/
kMinCardWidth.toInt();
return GridView.builder(
padding: const EdgeInsets.all(kEdgePadding),
shrinkWrap: true,
primary: false,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount:
nrOfColumns == 0 ? 1 : nrOfColumns,
mainAxisSpacing: kEdgePadding,
crossAxisSpacing: kEdgePadding,
childAspectRatio: 1.4,
),
itemCount: kMaxCards,
itemBuilder: (_, int index) => Card(
elevation: 3,
child: GridItem(
title: 'Card ${index + 1}',
color: Colors.primaries[
index % Colors.primaries.length][
Theme.of(context).brightness ==
Brightness.light
? 600
: 300],
),
),
);
},
)
],
),
),
),
),
),
),
],
);
}
}
// This hole thing could just have been a container and we would still have
// the same issue, but Ok at least it is a bit more representative of the use
// case where it was found like this.
class SideContent extends StatefulWidget {
const SideContent({
Key key,
this.isVisible,
this.menuWidth,
}) : super(key: key);
final bool isVisible;
final double menuWidth;
@override
_SideContentState createState() => _SideContentState();
}
class _SideContentState extends State<SideContent> {
int selectedItem;
@override
void initState() {
selectedItem = 2;
super.initState();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints size) {
return OverflowBox(
alignment: AlignmentDirectional.topStart,
minWidth: 0.0,
maxWidth: widget.menuWidth,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AppBar(
title: const Text('Side panel'),
leading: ConstrainedBox(
constraints: const BoxConstraints.tightFor(width: 56),
child: IconButton(
icon: const Icon(Icons.adjust),
onPressed: () {},
),
),
elevation: 0,
flexibleSpace: Container(
height: kToolbarHeight,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Theme.of(context).dividerColor),
),
),
child: null,
),
),
Expanded(
child: Container(
width: size.maxWidth,
decoration: BoxDecoration(
border: widget.isVisible
? BorderDirectional(
end: BorderSide(
color: Theme.of(context).dividerColor,
),
)
: null,
),
child: ListView(
padding:
const EdgeInsets.only(), // Removes all edge insets
children: <Widget>[
for (int i = 0; i < 8; i++)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SideItem(
width: size.maxWidth,
menuWidth: widget.menuWidth,
onTap: () {
setState(() {
selectedItem = i;
});
},
selected: selectedItem == i,
icon: Icons.account_circle,
label: "This is item $i",
showDivider: (i % 3 == 0) && i != 0,
),
],
),
],
),
),
),
],
),
);
},
);
}
}
// Totally don't need this either for the issue demo, but since I added it
// while looking for the cause, I't can stay to make the demo case a bit more
// realistic and it provides some Widgets for stress testing.
class SideItem extends StatelessWidget {
const SideItem({
Key key,
this.width,
this.menuWidth,
this.onTap,
this.selected = false,
this.icon,
this.label,
this.showDivider = false,
}) : super(key: key);
final double width;
final double menuWidth;
final VoidCallback onTap;
final bool selected;
final IconData icon;
final String label;
final bool showDivider;
@override
Widget build(BuildContext context) {
if (width < 5) {
return const SizedBox.shrink();
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (showDivider) const Divider(thickness: 1, height: 1),
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(0, 2, 5, 2),
child: Material(
clipBehavior: Clip.antiAlias,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(50 / 2.0),
bottomRight: Radius.circular(50 / 2.0),
),
color: selected
? Theme.of(context).colorScheme.primary.withAlpha(0x3d)
: Colors.transparent,
child: InkWell(
onTap: onTap,
child: SizedBox(
height: 50,
width: width - 5,
child: OverflowBox(
alignment: AlignmentDirectional.topStart,
minWidth: 0.0,
maxWidth: menuWidth,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
ConstrainedBox(
constraints: const BoxConstraints.tightFor(
width: 56, height: 56),
child: Icon(icon),
),
if (width < kShrinkWidth + 10)
const SizedBox.shrink()
else
Text(label)
],
),
),
),
),
),
),
],
);
}
}
}
// The grid items for the grid view with cards, not relevant for this case,
// just in the demo app as filler to stress it a bit more during resizing.
class GridItem extends StatelessWidget {
const GridItem({Key key, this.title, this.color}) : super(key: key);
final String title;
final Color color;
@override
Widget build(BuildContext context) {
final Color textColor =
ThemeData.estimateBrightnessForColor(color) == Brightness.light
? Colors.black87
: Colors.white70;
return Container(
color: color,
padding: const EdgeInsets.all(10),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
title,
style: TextStyle(
color: textColor,
fontSize: 18,
),
),
Icon(Icons.apps, color: textColor),
],
),
);
}
}
@rydmike
Copy link
Author

rydmike commented Oct 17, 2020

This is a sample code repo for Flutter issue:

flutter/flutter#67949

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment