Flutter架构初探
通过一个小问题引出本次分享
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Container(
child: FlatButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => SecondPage()));
},
child: Text('跳转')),
),
),
);
}
}
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
);
}
}
点击跳转按钮发现并没有跳转,还报了一个错误
Navigator operation requested with a context that does not include a Navigator.
Widget
在flutter中Everything is Widget足见Widget之重要性
Widget是什么??Widget在整个加载绘制的流程中扮演了什么角色??Widget可变吗??Widget可复用吗??Widget加载之后是怎么存储的??

Widget是不可变的配置文件,真正展示的不是他Widget是可以被复用的Widget本身不可变,但是可以通过State实现可变效果
BuildContext
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {}
}
StatelessWidget中有一个build方法传入了context,那么这个build方法是何时触发的,传入的context究竟为何物呢??
StatelessWidget
abstract class StatelessWidget extends Widget {
const StatelessWidget({ Key key }) : super(key: key);
@override
StatelessElement createElement() => StatelessElement(this);
@protected
Widget build(BuildContext context);
}
StatelessElement
class StatelessElement extends ComponentElement {
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();
}
}
现在我们知道context就是这个StatelessElement对象,StatelessElement中的build方法会触发StatelessWidget中的build方法,StatelessElement的build方法是Flutter Framework在构建UI树时隐式调用的
其继承关系为
class StatelessElement extends ComponentElement
abstract class ComponentElement extends Element
abstract class Element extends DiagnosticableTree implements BuildContext
使用BuildContext是不鼓励直接访问Element对象

MaterialApp & Navigator

我们看到Navigator是由MaterialApp提供的
flutter 渲染三棵树(Widget、Element、RenderObject)
我们创建的Widget被加载以后最终都会以树的形式存储,上面例子中的Widget Tree为

在Flutter中Widget Tree只是界面蓝图,提供界面展示的一些描述信息,Widget Tree中的每个节点多有一个与之对应的Element

我们回顾上面的问题
看意思是通过Navigator.of(context)没有找到Navigator
static NavigatorState of(
BuildContext context, {
bool rootNavigator = false,
bool nullOk = false,
}) {
final NavigatorState navigator = rootNavigator
? context.findRootAncestorStateOfType<NavigatorState>()
: context.findAncestorStateOfType<NavigatorState>();
assert(() {
if (navigator == null && !nullOk) {
throw FlutterError(
'Navigator operation requested with a context that does not include a Navigator.\n'
'The context used to push or pop routes from the Navigator must be that of a '
'widget that is a descendant of a Navigator widget.'
);
}
return true;
}());
return navigator;
}
我们看到这个方法其实获取的是NavigatorState对象
findRootAncestorStateOfType找到rootStatefindAncestorStateOfType找到距离自己最近的state

此时基本可以确定是context出问题了,根据传入的context找不到NavigatorState

知道原因之后很多的方法都能解决我们的问题 1、通过Key

2、封装成子Widget
class BtnWidget extends StatelessWidget{
@override
Widget build(BuildContext context) {
return FlatButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => SecondPage()));
},
child: Text('跳转'));
}
}
3、在上面的节点给他提供一个带Navigator的节点
void main() => runApp(MaterialApp(home: MyApp(),));
StatefulWidget组成的页面
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return MyAppState();
}
}
class MyAppState extends State<MyApp>{
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: FlatButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => SecondPage()));
},
child: Text('跳转')),
),
),
);
}
}
state是由StatefulElement触发StatefulWidget创建的

StatefulWidget并不持有State相反,State持有widget


再看一个小例子
void main() => runApp(Screen());
class Screen extends StatefulWidget {
@override
_ScreenState createState() => _ScreenState();
}
class _ScreenState extends State<Screen> {
List<Widget> widgets = [
StatelessContainer(),
StatelessContainer(),
];
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: widgets,
),
),
floatingActionButton: FloatingActionButton(
onPressed: switchWidget,
child: Icon(Icons.undo),
),
),
);
}
switchWidget(){
widgets.insert(0, widgets.removeAt(1));
setState(() {});
}
}
class StatelessContainer extends StatelessWidget {
final Color color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: color,
);
}
}
运行正常

但是如果把StatelessWidget改为StatefulWidget点击就没有响应了
class StatefulContainer extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _StatefulContainerState();
}
}
class _StatefulContainerState extends State<StatefulContainer> {
final Color color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: color,
);
}
}
Widget # canUpdate
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
Element通过持有的Widget来调用canUpdate方法,如果返回true那么只需要调整Widget就可以了,Element不需要重新创建,否则重新创建Element

我们先分析StatelessWidget的情况

两个Widget的runtimeType一致,所以返回true,只修改Widgets树就可以了,颜色信息存储在Widgets中,所以可以正常交换
分析StatefulWidget的情况

问题在于Widget交换了之后对于显示没有任何影响,因为color数据是存储在State里面的,而此时state对象并没有重新创建,所以交换Widget并不能达到交换显示颜色的目的。
解决方案一
List<Widget> widgets = [
Container(child: StatefulContainer(),,),
StatefulContainer(),
];
将其中一个StatefulContainer包一层Container,此时runtimeType不同canUpdate返回false,也就是需要重新构建Element,通过上面的分析可以知道Element的构造函数会触发State的重新创建,当重新创建State的时候颜色信息color也重新生成了,所以最终效果并不是颜色交换,而是每一次点击都重新获取了一个新的颜色

非但达不成我们的目的而且资源消耗也是很大的
解决方案二
List<Widget> widgets = [
StatefulContainer(key: UniqueKey(),),
StatefulContainer(key: UniqueKey(),),
];
此时runtimeType相同,但是key不同,所以和解决方案一相同,也会触发Element的重新构建,每一次点击都获取新的颜色信息
解决方案三
此时如果我们改成将color的获取放在Widget里面,State通过widget.color访问,会不会成功呢?
void main() => runApp(Screen());
class Screen extends StatefulWidget {
@override
_ScreenState createState() => _ScreenState();
}
class _ScreenState extends State<Screen> {
List<Widget> widgets = [
StatefulContainer(),
StatefulContainer(),
];
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: widgets,
),
),
floatingActionButton: FloatingActionButton(
onPressed: switchWidget,
child: Icon(Icons.undo),
),
),
);
}
switchWidget(){
widgets.insert(0, widgets.removeAt(1));
setState(() {});
}
}
class StatefulContainer extends StatefulWidget {
final Color color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
@override
State<StatefulWidget> createState() {
return _StatefulContainerState();
}
}
class _StatefulContainerState extends State<StatefulContainer> {
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: widget.color,
);
}
}
借用上图演示一下😄
此时runtimeType和key都相同(key为空就只比较runtimeType),所以canUpdate返回true,Element和State不需要重新创建,只是复用Widget就可以了,颜色信息color也是存储在Widget中的,所以可以达成我们的目的。
第三棵树 RenderObject树涉及到页面渲染内容比较多,我还没研究明白,大概意思是
- 先创建出
Widget Tree - 再通过
Widget的createElement创建出Element Tree并且和Widget Tree关联起来 - 通过
createRenderObject方法创建出renderObject树
真正负责渲染页面的是RenderObject,Element会持有Widget、State、RenderObject

参考文章
# How to Create Stateless Widgets-中文字幕
# How Stateful Widgets Are Used Best-中文字幕
# Flutter: WidgetTree、ElementTree
转载自:https://juejin.cn/post/7008083241978560542