flutter中 数据流Stream

本文同步发表于我的微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新

   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 参数控制重建

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值