Widget 分類
如果按照是否是有狀態(tài)的分類方式,那么Widget就分為StatelessWidget和StatefulWidget,StatelessWidget和StatefulWidget的Element都是ComponentElement,并且都不具備RenderObject。
他們UI的構(gòu)建都是調(diào)用build方法。區(qū)別就是StatelessWidget只是簡單的實現(xiàn)了ComponentElement,而StatefulWidget則復(fù)雜了許多,他的build是由_state去控制的,狀態(tài)和數(shù)據(jù)都保存在這里面, 這個在之前的文章中有提及。
StatelessElement代碼示例:
class StatelessElement extends ComponentElement {
/// Creates an element that uses the given widget as its configuration.
StatelessElement(StatelessWidget widget) : super(widget);
@override
StatelessWidget get widget => super.widget as StatelessWidget;
@override
Widget build() => widget.build(this);
@override
void update(StatelessWidget newWidget) {
super.update(newWidget);
assert(widget == newWidget);
_dirty = true;
rebuild();
}
}
可以看出在更新的時候也只是把_dirty臟標(biāo)記設(shè)置為true,然后就重新構(gòu)建。
State和Element的生命周期對比

State數(shù)據(jù)的傳遞

上面的代碼,當(dāng)點擊FloatingActionButton之后,最終顯示在屏幕上的文字是什么?為什么?
答案:
上面的代碼當(dāng)我們點擊按鈕之后,內(nèi)容并不會發(fā)生改變,因為StatePage的state已經(jīng)被創(chuàng)建過了,所以createState不會走兩次,故而data并不會發(fā)生改變(但是StatePage的data是發(fā)生了改變的),如果我們想使用更新之后的值,我們可以使用widte.data來引用。
class StatePage extends StatefulWidget {
StatePage({this.data});
final String data;
@override
_StatePageState createState() => _StatePageState();
}
class _StatePageState extends State<StatePage> {
@override
Widget build(BuildContext context) {
return Center(
child: Text(widget.data ?? ""),
);
}
}
setState是如何實現(xiàn)刷新的?
setState內(nèi)部會調(diào)用_element.markNeedsBuild();方法markNeedsBuild方法會在內(nèi)部把_dirty設(shè)置為true,然后加入到定時器當(dāng)中,然后在下一幀的WidgetsBinding.drawFrame才會被繪制。此處也可以得知,setState并不會馬上生效。
RenderObject分類
RenderBox
特性:會根據(jù)parent的constraints大小判斷自己的布局方法,然后將constraints傳遞給child得到child的大小,最后根據(jù)child返回的Size決定自己的Size,如果沒有child,就使用自己的Size。
他用于那些不涉及的滾動的控件布局,他的兩個關(guān)鍵參數(shù)就是BoxConstraints和Size。
RenderSliver
特性:因為其主要用于RenderViewport之后,里面涉及的運算和屬性對比RenderBox要復(fù)雜上許多。他的兩個關(guān)鍵參數(shù)是SliverConstraints和SliverGeometry。
SliverConstraints和BoxConstraints對比,BoxContraints只包括了,最大/最小的高度/寬度。但是SliverConstraints則更多的是滑動方向、滑動偏移、滑動容器大小、容器緩存大小和位置等相關(guān)參數(shù)。
Size和SliverGeometry進行對比,Size只包括了寬和高。但是SliverGeometry包括了滑動方位、繪制范圍、偏移等相關(guān)參數(shù)。
RenderBox和RenderSliver對比
RenderBox輸入輸出相較于RenderSliver更為簡單,RenderSliver更為關(guān)注滑動、方向、緩存等關(guān)鍵點,這也是因為其需要和ViewPort配合展示。例如我們經(jīng)常使用的ListView、GirdView、ScrollView等都是有Sliver和ViewPort組成的,可滑動的區(qū)域內(nèi)不可以直接使用RenderBox,如果一定要使用必須用RenderSliver進行嵌套后進行布局。

ViewPort
ViewPort根據(jù)自己的窗口的大小和偏移量,對child進行布局計算,通過對child輸入SliverConstraints來得到child的SliverGeometry,從而確定layout和paint等相關(guān)信息。
RenderSliver對應(yīng)的Sliver控件需要在ViewPort中使用。

當(dāng)外部的滑動事件產(chǎn)生時,就會觸發(fā)到ViewPort的markNeedsLayout方法,之后變化重新進行布局和繪制,并讓Sliver在ViewPort中進行偏移,達到看起來像是滑動了的效果。
RenderViewPort中為了避免性能消耗,對于滑動的時候內(nèi)部就會嘗試重新布局做了一個限制,最大的嘗試次數(shù)不能超過10次。
ListView、GridView內(nèi)部都是一個SliverList構(gòu)成,他們的children布局也是通過SliverList進行布局的。
RenderSliverList中,會通過傳入的ramainingCacheExtent、scrollOffset等參數(shù)去決定哪些child需要布局顯示,哪些child不需要被布局繪制,從而保證了列表中內(nèi)存優(yōu)化和良好的繪制性能。
單元素與多元素分類
根據(jù)Widget的child是否支持單個/多個child又可以分為SingleChildRenderObjectWidget和MultiChildRenderObjectWidget。
像我們經(jīng)常使用的Clip、Opacity、Padding、Align、SizededBox等都屬于SingleChildRenderObjectWidget;而Stack、Row、Column、RichText等則屬于MultiChildRenderObjectWidget。針對兩個不同的RenderObjectWidget,Flutter提供了CustomSingleChildLayout和CustomMultiChildLayout的抽象封裝。
SingleChildRenderObjectWidget
SingleChildRenderObjectWidget繼承RenderObjectWidget,因為只有一個child,所以實現(xiàn)起來相對簡單。繪制流程是通過RenderObject計算出自身的最大、最小寬高,并且通過performLayout綜合得到child返回的Size、最后在進行繪制。
MultiChildRenderObjectWidget

從上圖可以看出相較于SingleChildRenderObjectWidget,MultiChildRenderObjectWidget實現(xiàn)起來要復(fù)雜許多,主要復(fù)雜的部分在于RenderBox,我們需要自定義一個類繼承于RenderBox,同時還得混入ContainerRenderObjectMixin和RenderBoxContainerDefaultsMixin,然后去重寫他的兩個方法:setupParentData和performLayout,然后在重寫paint方法,調(diào)用系統(tǒng)繪制方法,完成繪制操作。
下面用一個實際例子來演示:
01- 創(chuàng)建ContainerBoxparentData
這個就是對應(yīng)上圖中右下方的抽象類(ConstainerBoxParentData)的具體實現(xiàn)
class RenderCloudParentData extends ContainerBoxParentData<RenderBox> {
/// 定義寬高
double width;
double height;
/// 通過offset和width、height得到一個矩形區(qū)域
Rect get content => Rect.fromLTWH(
offset.dx,
offset.dy,
width,
height,
);
}
02-創(chuàng)建RenderBox
這個就是對應(yīng)上圖的RenderBox的具體實現(xiàn)
/// 從類的定義就可以很好的看出,該類需要繼承于RenderBox,
/// 同時還需要混入ContainerRenderObjectMixin、RenderBoxContainerDefaultsMixin
class RenderCloudWidget extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, RenderCloudParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, RenderCloudParentData> {
/// 構(gòu)造方法
/// * children
/// * overflow 裁剪方式
/// * ratio 比例
RenderCloudWidget({
List<RenderBox> children,
Clip overflow = Clip.none,
double ratio,
}) : _ratio = ratio,
_overflow = overflow {
/// 這個是ContainerRenderObjectMixin的內(nèi)部方法,其內(nèi)部是一個雙線鏈表的結(jié)果,
/// 主要是用于快速定位下一個、上一個renderObject
addAll(children);
}
///圓周
double _mathPi = math.pi * 2;
///比例
double _ratio;
double get ratio => _ratio;
set ratio(double value) {
assert(value != null);
if (_ratio != value) {
_ratio = value;
markNeedsPaint();
}
}
/// 裁剪方式
Clip get overflow => _overflow;
set overflow(Clip value) {
assert(value != null);
if (_overflow != value) {
_overflow = value;
markNeedsPaint();
}
}
Clip _overflow;
/// 是否需要裁剪
bool _needClip = false;
/// 用于判斷是否重復(fù)區(qū)域了
bool overlaps(RenderCloudParentData data) {
Rect rect = data.content;
RenderBox child = data.previousSibling;
if (child == null) {
return false;
}
do {
RenderCloudParentData childParentData = child.parentData;
if (rect.overlaps(childParentData.content)) {
return true;
}
child = childParentData.previousSibling;
} while (child != null);
return false;
}
/// 這個就是需要重寫RenderBox其中的一個方法
@override
void setupParentData(covariant RenderObject child) {
if (child.parentData is! RenderCloudParentData) {
child.parentData = RenderCloudParentData();
}
}
/// 內(nèi)部布局方法,布局每一個child的位置大小
@override
void performLayout() {
///默認不需要裁剪
_needClip = false;
///沒有 childCount 不玩
if (childCount == 0) {
size = constraints.smallest;
return;
}
///初始化區(qū)域
var recordRect = Rect.zero;
var previousChildRect = Rect.zero;
RenderBox child = firstChild;
while (child != null) {
var curIndex = -1;
///提出數(shù)據(jù)
final RenderCloudParentData childParentData = child.parentData;
child.layout(constraints, parentUsesSize: true);
var childSize = child.size;
///記錄大小
childParentData.width = childSize.width;
childParentData.height = childSize.height;
do {
///設(shè)置 xy 軸的比例
var rX = ratio >= 1 ? ratio : 1.0;
var rY = ratio <= 1 ? ratio : 1.0;
///調(diào)整位置
var step = 0.02 * _mathPi;
var rotation = 0.0;
var angle = curIndex * step;
var angleRadius = 5 + 5 * angle;
var x = rX * angleRadius * math.cos(angle + rotation);
var y = rY * angleRadius * math.sin(angle + rotation);
var position = Offset(x, y);
///計算得到絕對偏移
var childOffset = position - Alignment.center.alongSize(childSize);
++curIndex;
///設(shè)置為遏制
childParentData.offset = childOffset;
///判處是否交疊
} while (overlaps(childParentData));
///記錄區(qū)域
previousChildRect = childParentData.content;
recordRect = recordRect.expandToInclude(previousChildRect);
///下一個
child = childParentData.nextSibling;
}
///調(diào)整布局大小
size = constraints
.tighten(
height: recordRect.height,
width: recordRect.width,
)
.smallest;
///居中
var contentCenter = size.center(Offset.zero);
var recordRectCenter = recordRect.center;
var transCenter = contentCenter - recordRectCenter;
child = firstChild;
while (child != null) {
final RenderCloudParentData childParentData = child.parentData;
childParentData.offset += transCenter;
child = childParentData.nextSibling;
}
///超過了嘛?
_needClip =
size.width < recordRect.width || size.height < recordRect.height;
}
/// 設(shè)置繪制默認
@override
void paint(PaintingContext context, Offset offset) {
if (!_needClip || _overflow == Clip.none) {
defaultPaint(context, offset);
} else {
context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
defaultPaint,
);
}
}
/// 觸摸測試,如果不想響應(yīng)就返回false,反正則是true
@override
bool hitTestChildren(HitTestResult result, {Offset position}) {
return defaultHitTestChildren(result, position: position);
}
}
03-創(chuàng)建Widget
主要是把RenderObject和Widget進行關(guān)聯(lián)起來
/// 創(chuàng)建Widget,繼承與MultiChildRenderObjectWidget
/// 主要是和之前的RenderBox關(guān)聯(lián)起來
class CloudWidget extends MultiChildRenderObjectWidget {
/// 自定義的相關(guān)屬性
final Clip overflow;
final double ratio;
/// 構(gòu)造方法
CloudWidget({
Key key,
this.ratio = -1,
this.overflow = Clip.none,
List<Widget> children = const <Widget>[],
}) : super(key: key, children: children);
/// 重寫創(chuàng)建RenderObject的方法,把之前創(chuàng)建的RenderCouldWidget返回
@override
RenderObject createRenderObject(BuildContext context) {
return RenderCloudWidget(ratio: ratio, overflow: overflow);
}
/// 在這里更新RenderCloudWidget的兩個關(guān)鍵參數(shù)
@override
void updateRenderObject(
BuildContext context, covariant RenderCloudWidget renderObject) {
/// ..表示級聯(lián)操作符
renderObject
..ratio = ratio
..overflow = overflow;
}
}
04-demo
///云詞圖
class CloudDemoPage extends StatefulWidget {
@override
_CloudDemoPageState createState() => _CloudDemoPageState();
}
class _CloudDemoPageState extends State<CloudDemoPage> {
///Item數(shù)據(jù)
List<CloudItemData> dataList = const <CloudItemData>[
CloudItemData('CloudGSY11111', Colors.amberAccent, 10, false),
CloudItemData('CloudGSY3333333T', Colors.limeAccent, 16, false),
CloudItemData('CloudGSYXXXXXXX', Colors.black, 14, true),
CloudItemData('CloudGSY55', Colors.black87, 33, false),
CloudItemData('CloudGSYAA', Colors.blueAccent, 15, false),
CloudItemData('CloudGSY44', Colors.indigoAccent, 16, false),
CloudItemData('CloudGSYBWWWWWW', Colors.deepOrange, 12, true),
CloudItemData('CloudGSY<<<', Colors.blue, 20, true),
CloudItemData('FFFFFFFFFFFFFF', Colors.blue, 12, false),
CloudItemData('BBBBBBBBBBB', Colors.deepPurpleAccent, 14, false),
CloudItemData('CloudGSY%%%%', Colors.orange, 20, true),
CloudItemData('CloudGSY%%%%%%%', Colors.blue, 12, false),
CloudItemData('CloudGSY&&&&', Colors.indigoAccent, 10, false),
CloudItemData('CloudGSYCCCC', Colors.yellow, 14, true),
CloudItemData('CloudGSY****', Colors.blueAccent, 13, false),
CloudItemData('CloudGSYRRRR', Colors.redAccent, 12, true),
CloudItemData('CloudGSYFFFFF', Colors.blue, 12, false),
CloudItemData('CloudGSYBBBBBBB', Colors.cyanAccent, 15, false),
CloudItemData('CloudGSY222222', Colors.blue, 16, false),
CloudItemData('CloudGSY1111111111111111', Colors.tealAccent, 19, false),
CloudItemData('CloudGSY####', Colors.black54, 12, false),
CloudItemData('CloudGSYFDWE', Colors.purpleAccent, 14, true),
CloudItemData('CloudGSY22222', Colors.indigoAccent, 19, false),
CloudItemData('CloudGSY44444', Colors.yellowAccent, 18, true),
CloudItemData('CloudGSY33333', Colors.lightBlueAccent, 17, false),
CloudItemData('CloudGSYXXXXXXXX', Colors.blue, 16, true),
CloudItemData('CloudGSYFFFFFFFF', Colors.black26, 14, false),
CloudItemData('CloudGSYZUuzzuuu', Colors.blue, 16, true),
CloudItemData('CloudGSYVVVVVVVVV', Colors.orange, 12, false),
CloudItemData('CloudGSY222223', Colors.black26, 13, true),
CloudItemData('CloudGSYGFD', Colors.yellow, 14, true),
CloudItemData('GGGGGGGGGG', Colors.deepPurpleAccent, 14, false),
CloudItemData('CloudGSYFFFFFF', Colors.blueAccent, 10, true),
CloudItemData('CloudGSY222', Colors.limeAccent, 12, false),
CloudItemData('CloudGSY6666', Colors.blue, 20, true),
CloudItemData('CloudGSY33333', Colors.teal, 14, false),
CloudItemData('YYYYYYYYYYYYYY', Colors.deepPurpleAccent, 14, false),
CloudItemData('CloudGSY 3 ', Colors.blue, 10, false),
CloudItemData('CloudGSYYYYYY', Colors.black54, 17, true),
CloudItemData('CloudGSYCC', Colors.lightBlueAccent, 11, false),
CloudItemData('CloudGSYGGGGG', Colors.deepPurpleAccent, 10, false)
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text("CloudDemoPage"),
),
body: new Center(
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.width,
///利用 FittedBox 約束 child
child: new FittedBox(
/// Cloud 布局
child: Container(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 6),
color: Colors.brown,
///布局
child: CloudWidget(
///容器寬高比例
ratio: 1,
children: <Widget>[
for (var item in dataList)
///判斷是否旋轉(zhuǎn)
RotatedBox(
quarterTurns: item.rotate ? 1 : 0,
child: Text(
item.text,
style: new TextStyle(
fontSize: item.size,
color: item.color,
),
),
),
],
),
),
),
),
),
);
}
}
class CloudItemData {
///文本
final String text;
///顏色
final Color color;
///旋轉(zhuǎn)
final bool rotate;
///大小
final double size;
const CloudItemData(
this.text,
this.color,
this.size,
this.rotate,
);
}
CustomMultiChildLayout
官方為了簡化我們實現(xiàn)自定義布局的方式,還提供了CustomMultiChildLayout這樣的類,這個類也是繼承了MultiChildRenderObjectWidget,并通過一個代理(MultiChildLayoutDelegate)來完成自定義UI相關(guān)的功能,通過這個代理,我們可以直接去重寫內(nèi)部的performLayout方法,從而達到我們自定布局的效果。
01-創(chuàng)建Delegate
class CircleLayoutDelegate extends MultiChildLayoutDelegate {
final List<String> customLayoutId;
final Offset center;
Size childSize;
CircleLayoutDelegate(
this.customLayoutId, {
this.center = Offset.zero,
this.childSize,
});
@override
void performLayout(Size size) {
for (var item in customLayoutId) {
if (hasChild(item)) {
double r = 100;
/// 下標(biāo)
int index = int.parse(item);
/// 均分
double step = 360 / customLayoutId.length;
/// 角度
double hd = (2 * math.pi / 360) * step * index;
var x = center.dx + math.sin(hd) * r;
var y = center.dy + math.cos(hd) * r;
/// 使用??= 避免多次賦值
childSize ??= Size(size.width / customLayoutId.length,
size.height / customLayoutId.length);
layoutChild(item, BoxConstraints.loose(childSize));
final double centerX = childSize.width * 0.5;
final double centerY = childSize.height * 0.5;
var result = Offset(x - centerX, y - centerY);
/// 設(shè)置child位置
positionChild(item, result);
}
}
}
@override
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) {
return true;
}
}
02-使用
class CustomMultiLayoutPage extends StatefulWidget {
@override
_CustomMultiLayoutPageState createState() => _CustomMultiLayoutPageState();
}
class _CustomMultiLayoutPageState extends State<CustomMultiLayoutPage> {
///用于 LayoutId 指定
///CircleLayoutDelegate 操作具體 Child 的 ChildId 是通過 LayoutId 指定的
List customLayoutId = ["0", "1", "2", "3", "4"].toList();
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final childSize = 66.0;
return Scaffold(
appBar: AppBar(),
body: Center(
child: Container(
color: Colors.yellowAccent,
width: size.width,
height: size.width,
child: CustomMultiChildLayout(
delegate: CircleLayoutDelegate(
customLayoutId,
childSize: Size(childSize, childSize),
center: Offset(size.width * 0.5, size.width * 0.5),
),
children: [
///使用 LayoutId 指定 childId
for (var item in customLayoutId)
new LayoutId(id: item, child: ContentItem(item, childSize)),
],
),
),
),
persistentFooterButtons: <Widget>[
TextButton(onPressed: () {
setState(() {
customLayoutId.add("${customLayoutId.length}");
});
},
child: Icon(Icons.add),
),
TextButton(onPressed: () {
setState(() {
if (customLayoutId.length > 1) {
customLayoutId.removeLast();
}
});
},
child: Icon(Icons.remove),
),
],
);
}
}
class ContentItem extends StatelessWidget {
final String text;
final double childSize;
ContentItem(this.text, this.childSize);
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(childSize / 2.0),
child: InkWell(
radius: childSize / 2.0,
customBorder: CircleBorder(),
onTap: () {},
child: Container(
width: childSize,
height: childSize,
child: Center(
child: Text(
text,
style: Theme.of(context)
.textTheme
.headline6
.copyWith(color: Colors.white),
),
),
),
),
);
}
}
效果圖

InheritedWidget共享狀態(tài)
InheritedWidge是Flutter Widget中非常重要的一個構(gòu)成部分,因為InheritedWidget常被用于數(shù)據(jù)共享。比如使用頻率很高的:Theme/ThemeData、Text/DefaultTextStyle、Slider/SliderTheme、Icon/IconTheme等內(nèi)部都是通過InheritedWidget實現(xiàn)數(shù)據(jù)共享的。并且Flutter中部分的狀態(tài)管理框架,內(nèi)部的狀態(tài)共享方法也是基于InheritedWidget去實現(xiàn)的。
InheritedWidget繼承自ProxyWidget,本身并不具備繪制的能力,但共享這個Widget等與共享Widget內(nèi)保存的數(shù)據(jù),獲取Widget就可以獲取到其內(nèi)部保存的數(shù)據(jù),如下圖:

每一個Element當(dāng)中都有一個成員變量:Map<Type, InheritedElement> _inheritedWidgets,改成員變量默認是空,之后當(dāng)父控件是InheritedWidget或者本身是InheritedWidget的時候才會初始化,當(dāng)父控件是InheritedWidget的時候,這個Map會逐級向下傳遞于合并。
那么context.inheritedFromWidgetOfExactType內(nèi)部做了啥呢?
通過查看Element的源碼截圖部分片段
@override
InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
assert(_debugCheckStateIsActiveForAncestorLookup());
/// 首先判斷是否有inheritedElement類型的數(shù)據(jù)
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
/// 找到了
if (ancestor != null) {
assert(ancestor is InheritedElement);
/// 添加到依賴集合中,并且通過updateDependencies將當(dāng)前的Element添加到_dependencies Map中,并且返回InheritedWidget
return inheritFromElement(ancestor, aspect: aspect);
}
_hadUnsatisfiedDependencies = true;
return null;
}
/// 下面兩個方法就是添加過程的實現(xiàn)
@override
InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
return dependOnInheritedElement(ancestor, aspect: aspect);
}
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
assert(ancestor != null);
/// 創(chuàng)建_dependencies
_dependencies ??= HashSet<InheritedElement>();
/// 添加InheritedElement到集合中
_dependencies.add(ancestor);
/// 跟新依賴
ancestor.updateDependencies(this, aspect);
/// 返回InheritedWidget
return ancestor.widget;
}
InheritedWidget是如何通知StatefulWidget進行更新的?
例如:當(dāng)我們在外界調(diào)用Theme.of(context)的時候,BuildContext的實現(xiàn)就是Element,所以當(dāng)內(nèi)部調(diào)用到context.inheritedFromWidgetOfExactType時,就會將context所代表的Element添加到InheritedElement的_dependents中,當(dāng)InheritedElement被更新的時候,就會觸發(fā)到齊內(nèi)部的notifyClients方法,該方法就會挨個遍歷被加入到_dependents,從而觸發(fā)到didChangeDependcies,然后就會更新UI
ErrorWidget 異常處理
在以往的開發(fā)中,當(dāng)我們程序拋出一些未處理的異?;蛘咤e誤的時候,就會引發(fā)程序的crash,但是在Flutter中則不會,這是因為Flutter中有一個全局處理的地方;
當(dāng)我們的代碼發(fā)生一些問題之后,在debug模式下可能會有某些或者整個頁面變成紅色,并顯示一些錯誤信息;在release模式下,則會顯示灰色的并沒有錯誤提示。
為了能讓我們的產(chǎn)品體驗更好,我們可以在main方法中做一些處理,讓錯誤看起來更加優(yōu)雅
void main() {
runZoned((){
ErrorWidget.builder = (FlutterErrorDetails details) {
Zone.current.handleUncaughtError(details.exception, details.stack);
return Container(color: Colors.orange,);
};
FlutterError.onError = (FlutterErrorDetails details) async {
FlutterError.dumpErrorToConsole(details);
Zone.current.handleUncaughtError(details.exception, details.stack);
};
runApp(MyApp());
}, onError: (Object obj, StackTrace stack) {
print(obj);
print(stack);
});
}