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
找到rootState
findAncestorStateOfType
找到距离自己最近的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