Flutter组件(Widget)的局部刷新方式
Flutter中有两个常用的状态Widget分为StatefulWidget和StatelessWidget,分别为动态视图和静态视图,视图的更新需要调用StatefulWidget的setState方法,这会遍历调用子Widget的build方法。如果一个页面内容比较复杂时,会包含多个widget,如果直接调用setState,会遍历所有子Widget的build,这样会造成很多不必要的开销,所以非常有必要了解Flutter中局部刷新的方式:
通过GlobalKey局部刷新
globalkey唯一定义了某个element,它使你能够访问与element相关联的其他对象,例如buildContext、state等。应用场景:跨widget访问状态。
例如:可以通过key.currentState拿到它的状态对象,然后就可以调用其中的onPressed方法。
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
GlobalKey<_TextWidgetState> textKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
TextWidget(textKey),// 需要更新的Text
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: (){
_counter ++;// 这里我们只给他值变动,状态刷新交给下面的key事件
textKey.currentState.onPressed(_counter);//这个counter的值已经改变了,但是没有重绘所以我们看到的知识我们定义的初始值
},
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class TextWidget extends StatefulWidget {
final Key key;
const TextWidget(this.key);
@override
_TextWidgetState createState() => _TextWidgetState();
}
class _TextWidgetState extends State<TextWidget> {
String _text = "0";
@override
Widget build(BuildContext context) {
return Center(child: Text(_text, style: TextStyle(fontSize: 20),),);
}
void onPressed(int count) {
setState(() {
_text = count.toString();
});
}
}
ValueNotifier和ValueListenableBuilder
Flutter框架内部提供了一个非常小巧精致的组件,专门用于局部组件的刷新。适用于值改动的刷新。
class ValueNotifierTestPage extends StatefulWidget {
@override
_ValueNotifierTestPageState createState() => _ValueNotifierTestPageState();
}
class _ValueNotifierTestPageState extends State<ValueNotifierTestPage> {
// 定义ValueNotifier<int> 对象 _counter
final ValueNotifier<int> _counter = ValueNotifier<int>(0);
void _incrementCounter(){
_counter.value += 1;
}
@override
Widget build(BuildContext context) {
return Center(
child: Scaffold(
appBar: AppBar(
title: Text('ValueNotifierTestPage'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
WidgetA(),
Text( 'You have pushed the button this many times:'),
ValueListenableBuilder<int>(
builder: _buildWithValue,
valueListenable: _counter,
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: Icon(Icons.add),
),
),
);
}
Widget _buildWithValue(BuildContext context, int value, Widget child) {
return Text(
'$value',
style: Theme.of(context).textTheme.headline4,
);
}
@override
void dispose() {
_counter.dispose();
super.dispose();
}
}
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('I am a widget that will not be rebuilt.');
}
}
实现原理:在 initState 中对传入的可监听对象进行监听,执行 _valueChanged 方法,_valueChanged 中进行了 setState 来触发当前状态的刷新。触发 build 方法,从而触发 widget.builder 回调,这样就实现了局部刷新。可以看到这里回调的 child 是组件传入的 child,所以直接使用,这就是对 child 的优化的根源。
可以看到 ValueListenableBuilder 实现局部刷新的本质,也是进行组件的抽离,让组件状态的改变框定在状态内部,并通过 builder 回调控制局部刷新,暴露给用户使用。
StatefulBuilder局部刷新
通过这个可以创建一个支持局部刷新的widget树,比如你可以在StatelessWidget里面刷新某个布局,但是不需要改变成StatefulWidget;也可以在StatefulWidget中使用做部分刷新而不需要刷新整个页面,这个刷新是不会调用Widget build(BuildContext context)刷新整个布局树的。
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
StateSetter _reloadTextSetter;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter){
_reloadTextSetter = stateSetter;
return Text(_counter.toString());
})// 需要更新的Text
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: (){
_counter ++;
_reloadTextSetter((){
});
},
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
FutureBuilder & StreamBuilder
异步UI更新:
很多时候我们会依赖一些异步数据来动态更新UI,比如在打开一个页面时我们需要先从互联网上获取数据,在获取数据的过程中显示一个加载框,等获取到数据时我们再渲染页面;又比如我们想展示Stream(比如文件流、互联网数据接收流)的进度。当然StatefulWidget我们完全可以实现以上功能。但由于在实际开发中依赖异步数据更新UI的这种场景非常常见,并且当StatefulWidget中控件树较大时,更新一个属性导致整个树重建,消耗性能,因此Flutter专门提供了FutureBuilder和SteamBuilder两个组件来快速实现这种功能。
class _MyHomePageState extends State<MyHomePage> {
Future<String> mockNetworkData() async {
return Future.delayed(Duration(seconds: 2), () => "我是从互联网上获取的数据");
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
FutureBuilder(
future: mockNetworkData(),
builder: (BuildContext context, AsyncSnapshot snapshot){
if(snapshot.connectionState == ConnectionState.done){
if(snapshot.hasError){
// 请求失败,显示错误
return Text("Error: ${snapshot.error}");
}else {
// 请求成功,显示数据
return Text("Contents: ${snapshot.data}");
}
}else {
return CircularProgressIndicator();
}
}),
],
),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class _MyHomePageState extends State<MyHomePage> {
Stream<int> counter(){
return Stream.periodic(Duration(seconds: 1), (i){
return i;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
StreamBuilder(
stream: counter(),
builder: (BuildContext context, AsyncSnapshot<int> snapshot){
if(snapshot.hasError){
return Text("Error: ${snapshot.error}");
}
switch (snapshot.connectionState){
case ConnectionState.none:
return Text("没有Stream");
case ConnectionState.waiting:
return Text("等待数据、、、");
case ConnectionState.active:
return Text("active: ${snapshot.data}");
case ConnectionState.done:
return Text("Stream已关闭");
}
return null;
}),
],
),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
InheritedWidget
通常情况下,子Widget无法单独感知父Widget的变化,当父state变化时,通过其build重建所有子widget;
InheriteddWidget可以避免这种全局创建,实现局部子Widget更新。InheritedWidget提供了一种在Widget树中从上到下传递、共享数据的方式。Flutter SDK正是通过InheritedWidget来共享应用主题和Locale等信息。
InheritedWidgetData
class InheritedWidgetData<T> extends InheritedWidget {
InheritedWidgetData({
Key key,
@required Widget child,
@required this.data,
}) : super(key: key, child: child);
final T data;
@override
bool updateShouldNotify(InheritedWidgetData<T> oldWidget) {
return true;
}
}
TestData
class TestData extends StatefulWidget {
TestData({
Key key,
this.child,
}) : super(key: key);
final Widget child;
@override
_TestDataState createState() => _TestDataState();
static _TestDataState of(BuildContext context, {bool rebuild = true}) {
if (rebuild) {
return context.dependOnInheritedWidgetOfExactType<InheritedWidgetData<_TestDataState>>().data;
}
return context.findAncestorWidgetOfExactType<InheritedWidgetData<_TestDataState>>().data;
}
}
class _TestDataState extends State<TestData> {
int counter = 0;
void _incrementCounter() {
setState(() {
counter++;
});
}
@override
Widget build(BuildContext context) {
return InheritedWidgetData(
data: this,
child: widget.child,
);
}
}
InheritedTest1Page
import 'package:flutter/material.dart';
class InheritedTest1Page extends StatefulWidget {
@override
_InheritedTest1PageState createState() => _InheritedTest1PageState();
}
class _InheritedTest1PageState extends State<InheritedTest1Page> {
@override
Widget build(BuildContext context) {
return Center(
child: Scaffold(
appBar: AppBar(
title: Text('InheritedWidgetTest'),
),
body: TestData(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(left: 10.0, top: 10.0, right: 10.0),
child: Text('我们常使用的\nTheme.of(context).textTheme\nMediaQuery.of(context).size等\n就是通过InheritedWidget实现的',
style: Theme.of(context).textTheme.title,),
),
WidgetA(),
WidgetB(),
WidgetC(),
],
),
),
),
);
}
}
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final _TestDataState state = TestData.of(context);
return Center(
child: Text(
'${state.counter}',
style: Theme.of(context).textTheme.display1,
),
);
}
}
class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('I am a widget that will not be rebuilt.');
}
}
class WidgetC extends StatelessWidget {
@override
Widget build(BuildContext context) {
final _TestDataState state = TestData.of(context, rebuild: false);
return RaisedButton(
onPressed: () {
state._incrementCounter();
},
child: Icon(Icons.add),
);
}
}
ChangeNotifierProvider(Provider)
provider是Google I/O 2019大会上宣布的现在官方推荐的管理方式,而ChangeNotifierProvider可以说是Provider的一种:
yaml文件需要引入provider: ^3.1.0
顶层嵌套ChangeNotifierProvider
void main(){
runApp(ChangeNotifierProvider(builder: (context) => DataInfo(), child: MyApp(),));
}
创建共享数据类DataInfo:
数据类需要with ChangeNotifier 以使用 notifyListeners()函数通知监听者更新界面。
class DataInfo with ChangeNotifier {
int _count = 0;
get count => _count;
addCount(){
_count ++;
notifyListeners();
}
subCount(){
_count --;
notifyListeners();
}
}
使用Provider.of(context)获取DataInfo
@override
Widget build(BuildContext context) {
var dataInfo = Provider.of<DataInfo>(context);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FloatingActionButton(
heroTag: "add",
child: Icon(Icons.add),
onPressed: (){
dataInfo.addCount();
}
),
Text(
'${dataInfo.count}',
),
FloatingActionButton(
heroTag: "sub",
child: Icon(Icons.add),
onPressed: (){
dataInfo.subCount();
})
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: (){
Navigator.of(context).push(CupertinoPageRoute(builder: (con){
return NextPage();
}));
},
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
nextPage:
使用Consumer包住需要使用共享数据的Widget
@override
Widget build(BuildContext context) {
return Consumer<DataInfo>(
builder: (context, dataInfo, _){
return Scaffold(
appBar: AppBar(
title: Text("next"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FloatingActionButton(
heroTag: "add",
child: Icon(Icons.add),
onPressed: (){
dataInfo.addCount();
}
),
Text(
'${dataInfo.count}',
),
FloatingActionButton(
heroTag: "sub",
child: Icon(Icons.add),
onPressed: (){
dataInfo.subCount();
})
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: (){
Navigator.of(context).pop();
},
tooltip: 'Increment',
child: Icon(Icons.settings_input_composite),
), // This trailing comma makes auto-formatting nicer for build methods.
);
},
);
}
RepaintBoundary
RepaintBoundary就是重绘边界,用于重绘时独立于父视图。页面需要更新的页面结构可以用 RepaintBoundary组件嵌套,flutter 会将包含的组件独立出一层"画布",去绘制。官方很多组件 外层也包了层 RepaintBoundary 标签。如果你的自定义view比较复杂,应该尽可能的避免重绘。
void markNeedsPaint() {
if (_needsPaint)
return;
_needsPaint = true;
if (isRepaintBoundary) { // 为true时,直接合成视图,避免重绘
if (owner != null) {
owner._nodesNeedingPaint.add(this);
owner.requestVisualUpdate();
}
} else if (parent is RenderObject) {
final RenderObject parent = this.parent;
parent.markNeedsPaint();
assert(parent == this.parent);
} else {
if (owner != null)
owner.requestVisualUpdate();
}
}
class RepainBoundaryPage extends StatefulWidget {
@override
_RepainBoundaryPageState createState() => _RepainBoundaryPageState();
}
class _RepainBoundaryPageState extends State<RepainBoundaryPage> {
@override
Widget build(BuildContext context) {
return Center(
child: Scaffold(
appBar: AppBar(
title: Text('RepainBoundaryPage'),
),
body: Column(
children: <Widget>[
WidgetA(),
WidgetB(),
WidgetC(),
],
),
),
);
}
}
class WidgetC extends StatefulWidget {
@override
_WidgetCState createState() => _WidgetCState();
}
class _WidgetCState extends State<WidgetC> {
int counter = 0;
@override
Widget build(BuildContext context) {
return RepaintBoundary(child: Column(
children: [
Text('~~~${counter}'),
RaisedButton(
onPressed: () {
setState(() {
counter++;
});
},
child: Icon(Icons.add),
)
],
),);
}
}
以上总结了几种Flutter的局部刷新的方式,可根据实际需要使用不同的方式,最适合的才是最好的。