StreamBuilder会更新ListView,但我发现有重复的条目
我的使用场景是在界面上按下一个按钮时,一个对象开始发送(有限的)数据,我想把这些数据追加到ListView,直到数据中包含一个标记,表示应该清空所有内容。
为了实现这一点,我让对象使用一个Stream来发送数据,我在UI中通过一个包裹ListView的 StreamBuilder来订阅它。
问题在于下面的测试代码会导致ListView出现重复项。
为了测试,下面的代码还包含一个Text(),由另一个StreamBuilder控制。我也订阅了该流,并在数据到达时将日志输出到控制台。这两者都没有重复项。
import 'package:flutter/material.dart';
import 'dart:async';
Stream<String> resultsStream() async* {
for (int i = 1; i <= 10; i++) {
await Future.delayed(Duration(seconds: 1));
yield "result: ${i}";
}
}
Stream<String> messagesStream() async* {
for (int i = 1; i <= 10; i++) {
await Future.delayed(Duration(seconds: 1));
yield "message: ${i}";
}
}
void main() {
StreamSubscription<String>? subscription = messagesStream().listen(
(data) {
print('subscription/console data: $data');
},
onError: (error) {
print('Error: $error');
},
onDone: () {
print('Stream is done (closed)!');
},
cancelOnError: false, // Don't cancel subscription if an error occurs
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: const Scaffold(body: SearchResultsWidget(passdata: {})),
);
}
}
class SearchResultsWidget extends StatefulWidget {
final Map<String, dynamic> passdata;
const SearchResultsWidget({super.key, required this.passdata});
@override
State<SearchResultsWidget> createState() => _SearchResultsWidgetState();
}
class _SearchResultsWidgetState extends State<SearchResultsWidget> {
@override
Widget build(BuildContext context) {
return Column(
children: [
const SizedBox(height: 35.0),
Text("messages"),
Container(
child: SizedBox(
height: 100,
child: StreamBuilder(
stream: messagesStream(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
print("text/message got data: ${snapshot.data}");
return Text(snapshot.data!);
}
},
),
),
),
/// list view
Text("rsults"),
Container(
child: SizedBox(
height: 100,
child: StreamBuilder(
stream: resultsStream(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
return ListView.builder(
itemBuilder: (context, index) {
print("listview/results got data: ${snapshot.data}");
return Text(snapshot.data.toString());
},
);
}
},
),
),
),
],
);
}
}
- 我是不是在ListView上做错了什么,有没有人能给出避免重复项的建议?
- 我还有一个疑问,据我理解,{AFAIU},StreamBuilder每次都会重新创建ListView。这是实现它的最有效的方式吗?
解决方案
查看 StreamBuilder 的文档,在“Managing the stream”部分中写道:
The stream must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the StreamBuilder. If the stream is created at the same time as the StreamBuilder, then every time the StreamBuilder's parent is rebuilt, the asynchronous task will be restarted.
这基本上意味着你需要在更早的状态中实例化流——例如 initState。
它可以分为三个步骤。我将通过代码示例来讲解它应该如何实现:
import 'package:flutter/material.dart';
import 'dart:async';
Stream<String> resultsStream() async* {
for (int i = 1; i <= 10; i++) {
await Future.delayed(const Duration(seconds: 1));
yield "result: $i";
}
}
Stream<String> messagesStream() async* {
for (int i = 1; i <= 10; i++) {
await Future.delayed(const Duration(seconds: 1));
yield "message: $i";
}
}
void main() {
StreamSubscription<String>? subscription = messagesStream().listen(
(data) {
print('subscription/console data: $data');
},
onError: (error) {
print('Error: $error');
},
onDone: () {
print('Stream is done (closed)!');
},
cancelOnError: false,
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Scaffold(body: SearchResultsWidget(passdata: {})),
);
}
}
class SearchResultsWidget extends StatefulWidget {
final Map<String, dynamic> passdata;
const SearchResultsWidget({super.key, required this.passdata});
@override
State<SearchResultsWidget> createState() => _SearchResultsWidgetState();
}
class _SearchResultsWidgetState extends State<SearchResultsWidget> {
// 1. Define the streams here
late final Stream<String> _messagesStream;
late final Stream<String> _resultsStream;
final List<String> _results = [];
@override
void initState() {
super.initState();
// 2. Initialize the streams once in initState
_messagesStream = messagesStream();
_resultsStream = resultsStream().map((event) {
_results.add(event);
return event;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
const SizedBox(height: 35.0),
const Text("messages"),
SizedBox(
height: 100,
child: StreamBuilder<String>(
// 3. Use the initialized stream reference
stream: _messagesStream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
return Text(snapshot.data!);
},
),
),
const Text("results"),
SizedBox(
height: 100,
child: StreamBuilder<String>(
stream: _resultsStream,
builder: (context, snapshot) {
if (!snapshot.hasData && _results.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: _results.length,
itemBuilder: (context, index) => Text(_results[index]),
);
},
),
),
],
);
}
}
也就是说:
- 将late final
Stream<String> _messagesStream和_resultsStream声明为状态字段。 - 在
initState() 中一次性初始化它们,并且不要在build()中调用流相关函数。 - 引用已存储的流。
相关:参见我在此处的回答,提供一个对类似情况的更深入解释:Flutter FutureBuilder不断触发并重建。它在每次重建时生成一个新的随机数 - 我该如何防止?