今天遇到一个很让人抓狂的问题,一直提示
1
type '(int, int) => Null' is not a subtype of type '(dynamic, dynamic) => void'
源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
typedef OnCurrentItemChanged<Type> = void Function(
Type prevItemAtPosition, Type currentItemAtPosition);
class ListenableScrollView<T> extends StatefulWidget {
final Offset currentChildPosition;
final OnCurrentItemChanged<T> onCurrentChildChanged;
final ScrollView child;
ListenableScrollView({
Key key,
@required this.child,
@required this.currentChildPosition,
@required this.onCurrentChildChanged,
T value,
}){
print("$onCurrentChildChanged");
}
@override
_ListenableScrollViewState createState() => _ListenableScrollViewState<T>();
}
class _ListenableScrollViewState<T> extends State<ListenableScrollView> {
T previous;
T current;
@override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: _onNotification,
child: widget.child,
);
}
bool _onNotification(Notification notification) {
if (notification is ScrollEndNotification) {
Future.microtask(() {
var renderBox = context.findRenderObject() as RenderBox;
var offset = renderBox.localToGlobal(Offset(
widget.currentChildPosition.dx,
widget.currentChildPosition.dy,
));
HitTestResult result = HitTestResult();
WidgetsBinding.instance.hitTest(result, offset);
for (var i in result.path) {
if (i.target is RenderMetaData) {
var d = i.target as RenderMetaData;
if (d.metaData is T) {
previous = current;
current = d.metaData;
setState(() {
widget.onCurrentChildChanged(previous, current);
});
}
}
}
});
}
return true;
}
}
测试代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
void main() {
testWidgets('ListenableScrollView', (WidgetTester tester) async {
int current;
int previous;
double itemHeight = 100;
/// GIVEN
ListenableScrollView<int> scrollView = ListenableScrollView<int>(
child: ListView.builder(
itemCount: 10,
itemBuilder: (ctx,index){
return MetaData(
metaData: index,
child: Container(
height: itemHeight,
padding: EdgeInsets.only(top: 10,bottom: 10),
color: Colors.green,
child: Center(child: Text("$index"),),
),
);
}),
currentChildPosition: Offset(10, 30),
onCurrentChildChanged: (prev,cur) {
print("onCurrentChildChanged: $prev $cur");
previous = prev;
current = cur;
}
);
/// TextDirection is provided by a MaterialApp or a CupertinoApp.
/// You need to wrap your widget with a MaterialApp widget!
MaterialApp app = MaterialApp(
home: SafeArea(child: scrollView,),
);
/// WHEN
await tester.pumpWidget(app);
for (int i =0;i<3;++i){
final gesture = await tester.startGesture(Offset(0, 10));
await gesture.moveBy(Offset(0, -itemHeight+10));
await gesture.up();
await tester.pump();
/// THEN
if(i > 0) {
expect(previous, equals(i - 1));
}
expect(current, equals(i));
}
});
}
答案说出来其实一文不值: widget.OnCurrentItemChanged
的类型就是 void Function(dynamic,dynamic) 。因为 widget 的类型是由 State定义的,而 State 并没有 T 的信息,因此,T 默认为 dynamic 。所以,修改的方案是:
1
(widget as ListenableScrollView<T>).OnCurrentItemChanged
当然,这样写明显不够优雅,更优雅的办法是,把widget 重写:
1
2
3
4
5
6
7
8
class _ListenableScrollViewState<T> extends State<ListenableScrollView> {
T previous;
T current;
@override
ListenableScrollView<T> get widget => super.widget as ListenableScrollView<T>;
...
}