本文同步发表于我的微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新
Flutter 中,Stream 是一个非常重要的异步编程,代表一系列异步事件的序列。与 Future 返回单个异步结果不同,Stream 可以持续地产生多个值。
1. Stream特点
-
异步数据序列:按时间顺序排列的数据流
-
可监听:可以订阅并接收数据、错误和完成信号
-
可控制:可以暂停、恢复、取消订阅
2. 创建 Stream 方式
2.1 使用 async* 生成器
Stream<String> fetchUserMessages(String userId) async* {
final messages = [
'欢迎回来!',
'您有3条新消息',
'系统维护通知',
'账户验证成功'
];
for (final message in messages) {
// 模拟网络延迟
await Future.delayed(Duration(seconds: 1));
yield '$message [用户: $userId]';
}
}
2.2 使用 StreamController
class DataFeedService {
final StreamController<StockPrice> _priceController =
StreamController<StockPrice>.broadcast();
Stream<StockPrice> get priceStream => _priceController.stream;
void simulateMarketData() {
const symbols = ['AAPL', 'GOOGL', 'TSLA', 'MSFT'];
Timer.periodic(Duration(seconds: 2), (timer) {
final random = Random();
final symbol = symbols[random.nextInt(symbols.length)];
final price = StockPrice(
symbol: symbol,
price: 100 + random.nextDouble() * 1000,
timestamp: DateTime.now(),
change: (random.nextDouble() - 0.5) * 20
);
_priceController.add(price);
});
}
void dispose() {
_priceController.close();
}
}
class StockPrice {
final String symbol;
final double price;
final DateTime timestamp;
final double change;
StockPrice({
required this.symbol,
required this.price,
required this.timestamp,
required this.change,
});
}
2.3 从 Future 列表创建
Stream<WeatherData> fetchMultipleCityWeather(List<String> cities) {
return Stream.fromFutures(
cities.map((city) => _fetchSingleCityWeather(city))
);
}
Future<WeatherData> _fetchSingleCityWeather(String city) async {
await Future.delayed(Duration(seconds: 1));
final random = Random();
return WeatherData(
city: city,
temperature: 15 + random.nextInt(25),
humidity: 30 + random.nextInt(70),
condition: ['晴朗', '多云', '小雨', '大雪'][random.nextInt(4)]
);
}
3. Stream 的listen方法
class StockPriceWidget extends StatefulWidget {
@override
_StockPriceWidgetState createState() => _StockPriceWidgetState();
}
class _StockPriceWidgetState extends State<StockPriceWidget> {
final DataFeedService _dataService = DataFeedService();
StreamSubscription<StockPrice>? _subscription;
List<StockPrice> _latestPrices = [];
@override
void initState() {
super.initState();
_startListening();
_dataService.simulateMarketData();
}
void _startListening() {
_subscription = _dataService.priceStream.listen(
(StockPrice price) {
setState(() {
// 更新最新价格,只保留最近5条
_latestPrices.insert(0, price);
if (_latestPrices.length > 5) {
_latestPrices.removeLast();
}
});
},
onError: (error) {
print('数据流错误: $error');
},
onDone: () {
print('数据流结束');
},
cancelOnError: false,
);
}
void _pauseSubscription() {
_subscription?.pause();
}
void _resumeSubscription() {
_subscription?.resume();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: [
ElevatedButton(
onPressed: _pauseSubscription,
child: Text('暂停接收'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: _resumeSubscription,
child: Text('继续接收'),
),
],
),
Expanded(
child: ListView.builder(
itemCount: _latestPrices.length,
itemBuilder: (context, index) {
final price = _latestPrices[index];
return ListTile(
leading: Icon(
price.change >= 0 ? Icons.trending_up : Icons.trending_down,
color: price.change >= 0 ? Colors.green : Colors.red,
),
title: Text(price.symbol),
subtitle: Text('时间: ${price.timestamp.toString()}'),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('\$${price.price.toStringAsFixed(2)}'),
Text(
'${price.change >= 0 ? '+' : ''}${price.change.toStringAsFixed(2)}',
style: TextStyle(
color: price.change >= 0 ? Colors.green : Colors.red,
),
),
],
),
);
},
),
),
],
);
}
@override
void dispose() {
_subscription?.cancel();
_dataService.dispose();
super.dispose();
}
}
4. Stream 常用转换操作
class StreamOperationsExample {
final StreamController<int> _numberController = StreamController<int>();
void demonstrateOperations() {
// 原始数据流:1, 2, 3, 4, 5...
final originalStream = _numberController.stream;
// 1. where - 过滤
final evenNumbers = originalStream.where((number) => number % 2 == 0);
// 2. map - 转换
final squaredNumbers = originalStream.map((number) => number * number);
// 3. take - 限制数量
final firstThree = originalStream.take(3);
// 4. skip - 跳过元素
final afterFirstTwo = originalStream.skip(2);
// 5. distinct - 去重
final uniqueNumbers = originalStream.distinct();
// 6. asyncMap - 异步转换
final asyncProcessed = originalStream.asyncMap((number) async {
await Future.delayed(Duration(milliseconds: 100));
return '处理后的数字: $number';
});
// 7. expand - 展开
final expanded = originalStream.expand((number) => [number, number + 0.5]);
// 8. transform - 自定义转换
final customTransformed = originalStream.transform(
_createCustomTransformer()
);
}
StreamTransformer<int, String> _createCustomTransformer() {
return StreamTransformer<int, String>.fromHandlers(
handleData: (number, sink) {
if (number < 0) {
sink.addError('负数不允许: $number');
} else if (number > 100) {
sink.add('大数字: $number');
} else {
sink.add('普通数字: $number');
}
},
handleError: (error, stackTrace, sink) {
sink.add('错误处理: $error');
},
handleDone: (sink) {
sink.add('流处理完成');
sink.close();
},
);
}
}
5. StreamBuilder 的使用
class WeatherDashboard extends StatelessWidget {
final Stream<WeatherData> weatherStream;
WeatherDashboard({required this.weatherStream});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('城市天气监控'),
backgroundColor: Colors.blue[700],
),
body: StreamBuilder<WeatherData>(
stream: weatherStream,
builder: (context, snapshot) {
// 处理不同的连接状态
if (snapshot.connectionState == ConnectionState.waiting) {
return _buildLoadingState();
} else if (snapshot.hasError) {
return _buildErrorState(snapshot.error.toString());
} else if (snapshot.hasData) {
return _buildWeatherDisplay(snapshot.data!);
} else {
return _buildInitialState();
}
},
),
);
}
Widget _buildLoadingState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 20),
Text(
'正在获取天气数据...',
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
],
),
);
}
Widget _buildErrorState(String error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.red),
SizedBox(height: 20),
Text(
'数据获取失败',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
Padding(
padding: EdgeInsets.symmetric(horizontal: 40),
child: Text(
error,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[600]),
),
),
SizedBox(height: 20),
ElevatedButton.icon(
onPressed: () {
// 重新加载逻辑
},
icon: Icon(Icons.refresh),
label: Text('重新尝试'),
),
],
),
);
}
Widget _buildWeatherDisplay(WeatherData weather) {
return Padding(
padding: EdgeInsets.all(20),
child: Card(
elevation: 8,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.wb_sunny, size: 64, color: Colors.amber),
SizedBox(height: 16),
Text(
weather.city,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.blue[800],
),
),
SizedBox(height: 8),
Text(
weather.condition,
style: TextStyle(fontSize: 18, color: Colors.grey[600]),
),
SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildWeatherInfo('温度', '${weather.temperature}°C', Icons.thermostat),
_buildWeatherInfo('湿度', '${weather.humidity}%', Icons.water_drop),
],
),
],
),
),
),
);
}
Widget _buildWeatherInfo(String label, String value, IconData icon) {
return Column(
children: [
Icon(icon, size: 32, color: Colors.blue[600]),
SizedBox(height: 8),
Text(
label,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
Text(
value,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
);
}
Widget _buildInitialState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.cloud_queue, size: 64, color: Colors.grey[400]),
SizedBox(height: 20),
Text(
'等待天气数据...',
style: TextStyle(fontSize: 16, color: Colors.grey[500]),
),
],
),
);
}
}
6. 注意事项
-
对高频数据流使用
debounce或throttle -
使用
share()或shareValue()避免重复计算 -
在不需要时及时取消订阅
-
使用
StreamBuilder的buildWhen参数控制重建
1588

被折叠的 条评论
为什么被折叠?



