Flutter局部刷新 ValueNotifier和ValueListenableBuilder
1.介绍
在上一篇中Provider,我们介绍了一个Widget Selector,它的目的是为了减少setState((){})带来的全局刷新问题,对于复杂的页面来说,如果仅仅只是其中一小块发生改变,就触发大面积的刷新,很大可能会带来很大的性能问题以及耗电问题
因此官方提供了`ValueNotifier`来进行刷新。ValueNotifier不仅仅可以进行单个组件的刷新,也可以同时被多个组件进行监听,还可以对自定义数据进行处理
适用范围:适用某个对象(int/float/double或者Bean对象)ValueNotifier是轻量级的状态管理组件,适用于一些常用小组件的封装
2.如何使用
监听器:
ValueNotifier<int> _valueNotifier = ValueNotifier<int>(0);//数据监听器
被监听的地方
ValueListenableBuilder<int>(
valueListenable: _valueNotifier,
builder: (BuildContext context, int value, Widget child){
print('YM----ChildWidget1-----ValueListenableBuilder----进行刷新啊');
return Text('ChildWidget1新值:$value');
},
// child: Container(
// child: Text('子控件'),
// ),
);
属性解答:
- builder 在数据发生变化时适用
- valueListenable 监听器
- child:会回传给builder中的child,可以为null
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Material App',
home: Scaffold(
appBar: AppBar(
title: Text('Material App Bar'),
),
body: Center(
child: Container(
child: Column(
children: [
MyHomePage(
title: "ValueNotifier",
),
],
),
),
),
),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
MyHomePage({Key key, this.title}) : super(key: key);
@override
_MyHomePageState createState() {
return _MyHomePageState();
}
}
class _MyHomePageState extends State<MyHomePage> {
// 1.定义 ValueNotifier 对象 _counter
final ValueNotifier<int> _counter = ValueNotifier<int>(0);
@override
void dispose() {
_counter.dispose();
super.dispose();
}
void add() {
_counter.value += 1;
}
Widget _buildValue(BuildContext context, int value, Widget child) {
print("_buildValue2刷新了");
return Text(
'$value',
style: Theme.of(context).textTheme.headline4,
);
}
Widget _buildValue2(BuildContext context, int value, Widget child) {
print("_buildValue2刷新了");
return Text(
'$value',
style: Theme.of(context).textTheme.headline4,
);
}
@override
Widget build(BuildContext context) {
print("重新刷新了");
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ValueListenableBuilder<int>(
builder: _buildValue,
valueListenable: _counter,
),
ValueListenableBuilder<int>(
builder: _buildValue2,
valueListenable: _counter,
),
FloatingActionButton(
onPressed: add,
child: Icon(Icons.add),
),
],
),
);
}
}
解答:ValueNotifier本质还是ChangeNotifier(类比Android ViewModel和LiveData),ValueListenableBuilder 还需要一个 builder
,对应于Provider中Consumer
每当监听的对象值发生变化时,会触发builder
方法进行刷新。在点击时只需要改变 _counter.value
的值,就会触发 _buildWithValue
从而将界面数字刷新。
我们这样就减少了很对绘制的时间,并提升了绘制性能
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
ValueNotifier(this._value);
@override
T get value => _value;
T _value;
set value(T newValue) {
if (_value == newValue)
return;
_value = newValue;
notifyListeners();
}
@override
String toString() => '${describeIdentity(this)}($value)';
}
3.局部刷新的思考
问题1:多个StatefulWidget包裹时,SA包裹SB,SB包裹SC,例如,许多Button都有水波纹以及闪烁的颜色,此时为什么没有触发全局的刷新,Button肯定执行了内部的SetState来刷新实现的。
解答:这是一种组件分离的效果,SC(Button)中setState,状态变化的刷新封装在自己组件内,向外界提供操作接口,则只会影响到本身的组件,父组件不会影响到,如果在父组件SetState,则影响到父组件以及父组件下的所有子组件。我们专门来处理这种情况。
4.关于 child的理解
关于背景的刷新,有点小门道。这里会体现出 ValueListenableBuilder中child
属性的作用。 主页内容放入 child 属性中,那么在触发 builder
时,会直接使用这个 child,不会再构建一遍 child
。
总结:ValueListenableBuilder中如果child 有,则builder中不会再次创建这个child,child只会生成一次,可以减少刷新,这也就是child的由来
5.适用场景
监听ScrollVIew等滑动Widget,当滑动时,可以监听它们滑动的位置,然后通过ValueListenableBuilder进行局部的刷新,这样效率更多,性能更高
案例:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
home: Scaffold(
body: Example508(),
));
}
}
class Example508 extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _ExampleState();
}
}
class _ExampleState extends State<Example508> {
var imgList = [
'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2877516247,37083492&fm=26&gp=0.jpg',
'https://www.itying.com/images/flutter/1.png'
];
// 页数监听对象
final ValueNotifier<int> page = ValueNotifier<int>(1);
// 进度监听对象
final ValueNotifier<double> factor = ValueNotifier<double>(1 / 2);
/// 初始化控制器
PageController pageController;
//PageView当前显示页面索引
int currentPage = 0;
Color get startColor => Colors.red; // 起点颜色
Color get endColor => Colors.blue; // 终点颜色
@override
void initState() {
super.initState();
//创建控制器的实例
pageController = new PageController(
viewportFraction: 0.9,
//用来配置PageView中默认显示的页面 从0开始
initialPage: 0,
//为true是保持加载的每个页面的状态
keepPage: true,
);
///PageView设置滑动监听
pageController.addListener(() {
// //PageView滑动的距离
// double offset = pageController.offset;
// //当前显示的页面的索引
// double page = pageController.page;
// print("pageView 滑动的距离 $offset 索引 $page");
double value =
(pageController.page + 1) % imgList.length / imgList.length;
factor.value = value == 0 ? 1 : value;
print("pageView 滑动的距离 ${factor.value}");
});
}
Widget _buildTitle(BuildContext context) {
print('---------_buildTitle------------');
return Container(
alignment: Alignment.center,
height: MediaQuery.of(context).size.height * 0.25,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.api,
color: Colors.white,
size: 45,
),
SizedBox(
width: 20,
),
ValueListenableBuilder(
valueListenable: page,
builder: _buildWithPageChange,
),
],
),
);
}
Widget _buildWithPageChange(BuildContext context, int value, Widget child) {
return Text(
"绘制集录 ${value / imgList.length}",
style: TextStyle(fontSize: 30, color: Colors.white),
);
}
Widget _buildProgress() => Container(
margin: EdgeInsets.only(bottom: 12, left: 48, right: 48, top: 10),
height: 2,
child: ValueListenableBuilder(
valueListenable: factor,
builder: (context, value, child) {
return LinearProgressIndicator(
value: factor.value,
valueColor: AlwaysStoppedAnimation(
Colors.red,
),
);
},
),
);
Widget _buildPageView() {
return Container(
height: 200,
child: PageView.builder(
//当页面选中后回调此方法
//参数[index]是当前滑动到的页面角标索引 从0开始
onPageChanged: (int index) {
page.value = index + 1;
},
//值为flase时 显示第一个页面 然后从左向右开始滑动
//值为true时 显示最后一个页面 然后从右向左开始滑动
reverse: false,
//滑动到页面底部无回弹效果
physics: BouncingScrollPhysics(),
//纵向滑动切换
scrollDirection: Axis.horizontal,
//页面控制器
controller: pageController,
//所有的子Widget
itemBuilder: (BuildContext context, int index) {
return Container(
height: 200,
//缩放的图片
width: MediaQuery.of(context).size.width,
child: Image.network(
imgList[index % 2],
),
);
},
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ValueListenableBuilder(
valueListenable: factor,
builder: (_, value, child) => Container(
height: 500,
color: Color.lerp(startColor, endColor, value),
child: child, //<--- tag1
),
child: Column(
children: [
_buildTitle(context),
_buildPageView(),
SizedBox(
height: 20,
),
_buildProgress(),
],
),
));
}
}
备注:PageView.builder一定要有高度,一般设置在Container中,否则会报错,hasSize
颜色可以通过 Color.lerp
来计算两个颜色之间对应分度值的颜色,这样就实现了,背景动态换肤