Flutter鸿蒙开发:股票行情查看实战教程 - OpenHarmony跨平台指南

Flutter 三方库 cached_network_image 的鸿蒙化适配与实战指南
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

本文详细介绍如何在Flutter鸿蒙应用中实现股票行情查看功能,包含大盘指数、股票列表、分时图表、自选股管理等功能。

一、前言

股票行情查看是投资者日常必备的功能,通过股票行情应用可以实时了解股市动态。本文将介绍如何使用Flutter开发股票行情查看应用,支持大盘指数、个股行情、自选股管理等功能。

二、效果展示

在这里插入图片描述

2.1 功能特性

功能 描述
大盘指数 显示上证、深证、创业板指数
股票列表 展示股票价格、涨跌幅、成交量
分时图表 显示股票分时走势图
自选股 支持添加自选股票
涨跌榜 按涨跌幅排序展示股票
搜索功能 快速搜索股票代码或名称

三、项目背景与目标

3.1 项目背景

随着移动互联网的发展,越来越多的投资者选择通过手机应用查看股票行情。股票行情应用需要提供实时、准确的行情数据,以及便捷的操作体验。

3.2 项目目标

  • 实现大盘指数展示
  • 提供股票列表和详情查看
  • 支持自选股管理
  • 实现涨跌榜排序功能

四、技术架构设计

4.1 架构概述

股票行情应用采用Flutter跨平台框架开发,主要包含以下模块:

  • 大盘指数模块:展示上证、深证、创业板指数
  • 股票列表模块:展示股票行情数据
  • 分时图表模块:绘制股票分时走势图
  • 自选股模块:管理用户自选股票

4.2 技术原理

使用CustomPainter绘制分时图表,通过模拟数据生成股票行情,ModalBottomSheet实现股票详情弹窗。

五、详细实现

5.1 Flutter端实现

import 'package:flutter/material.dart';
import 'dart:math';

class StockMarketPage extends StatefulWidget {
  const StockMarketPage({super.key});

  
  State<StockMarketPage> createState() => _StockMarketPageState();
}

class _StockMarketPageState extends State<StockMarketPage> {
  String _selectedTab = '自选';
  final TextEditingController _searchController = TextEditingController();

  final List<StockInfo> _stocks = [
    StockInfo(code: '000001', name: '平安银行', price: 12.35, change: 2.15, volume: '1.2亿'),
    StockInfo(code: '000002', name: '万科A', price: 8.92, change: -1.23, volume: '8560万'),
    StockInfo(code: '600519', name: '贵州茅台', price: 1856.00, change: 0.85, volume: '325万'),
    StockInfo(code: '601318', name: '中国平安', price: 45.67, change: -0.45, volume: '4560万'),
    StockInfo(code: '000858', name: '五粮液', price: 156.78, change: 1.56, volume: '2890万'),
    StockInfo(code: '002415', name: '海康威视', price: 32.45, change: 3.21, volume: '6780万'),
    StockInfo(code: '600036', name: '招商银行', price: 35.89, change: 0.78, volume: '5120万'),
    StockInfo(code: '601166', name: '兴业银行', price: 18.56, change: -0.32, volume: '3450万'),
  ];

  final List<StockInfo> _favoriteStocks = [];

  List<double> _generateChartData() {
    final random = Random();
    return List.generate(30, (i) => 100 + random.nextDouble() * 50);
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('股票行情'),
        backgroundColor: Colors.red,
        actions: [IconButton(icon: const Icon(Icons.search), onPressed: () => _showSearch())],
      ),
      body: Column(
        children: [
          _buildMarketOverview(),
          _buildTabBar(),
          Expanded(child: _buildStockList()),
        ],
      ),
    );
  }

  Widget _buildMarketOverview() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(gradient: LinearGradient(colors: [Colors.red.shade400, Colors.red.shade600])),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          _buildIndexItem('上证指数', 3256.78, 1.23),
          _buildIndexItem('深证成指', 10856.45, 0.85),
          _buildIndexItem('创业板指', 2156.32, -0.45),
        ],
      ),
    );
  }

  Widget _buildIndexItem(String name, double value, double change) {
    return Column(
      children: [
        Text(name, style: const TextStyle(color: Colors.white70, fontSize: 12)),
        const SizedBox(height: 4),
        Text(value.toStringAsFixed(2), style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
        Text('${change > 0 ? '+' : ''}${change}%', style: TextStyle(color: change >= 0 ? Colors.white : Colors.green.shade200, fontSize: 12)),
      ],
    );
  }

  Widget _buildTabBar() {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: Row(
        children: ['自选', '涨幅榜', '跌幅榜', '热门'].map((tab) {
          final isSelected = _selectedTab == tab;
          return Expanded(
            child: GestureDetector(
              onTap: () => setState(() => _selectedTab = tab),
              child: Container(
                padding: const EdgeInsets.symmetric(vertical: 8),
                decoration: BoxDecoration(color: isSelected ? Colors.red : Colors.transparent, borderRadius: BorderRadius.circular(8)),
                child: Text(tab, textAlign: TextAlign.center, style: TextStyle(color: isSelected ? Colors.white : Colors.black87, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal)),
              ),
            ),
          );
        }).toList(),
      ),
    );
  }

  Widget _buildStockList() {
    List<StockInfo> displayStocks = _stocks;
    if (_selectedTab == '自选') {
      displayStocks = _favoriteStocks.isEmpty ? _stocks.take(3).toList() : _favoriteStocks;
    } else if (_selectedTab == '涨幅榜') {
      displayStocks = List.from(_stocks)..sort((a, b) => b.change.compareTo(a.change));
    } else if (_selectedTab == '跌幅榜') {
      displayStocks = List.from(_stocks)..sort((a, b) => a.change.compareTo(b.change));
    }

    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemCount: displayStocks.length,
      itemBuilder: (context, index) => _buildStockCard(displayStocks[index]),
    );
  }

  Widget _buildStockCard(StockInfo stock) {
    final isUp = stock.change >= 0;
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: InkWell(
        onTap: () => _showStockDetail(stock),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(stock.name, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
                    Text(stock.code, style: TextStyle(color: Colors.grey.shade600, fontSize: 12)),
                  ],
                ),
              ),
              SizedBox(
                width: 80,
                height: 40,
                child: CustomPaint(painter: MiniChartPainter(_generateChartData(), isUp ? Colors.red : Colors.green)),
              ),
              const SizedBox(width: 12),
              Column(
                crossAxisAlignment: CrossAxisAlignment.end,
                children: [
                  Text(stock.price.toStringAsFixed(2), style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: isUp ? Colors.red : Colors.green)),
                  Container(
                    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
                    decoration: BoxDecoration(color: isUp ? Colors.red : Colors.green, borderRadius: BorderRadius.circular(4)),
                    child: Text('${isUp ? '+' : ''}${stock.change}%', style: const TextStyle(color: Colors.white, fontSize: 12)),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }

  void _showStockDetail(StockInfo stock) {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      builder: (context) {
        return DraggableScrollableSheet(
          initialChildSize: 0.7,
          minChildSize: 0.5,
          maxChildSize: 0.95,
          expand: false,
          builder: (context, scrollController) {
            return Container(
              padding: const EdgeInsets.all(20),
              child: Column(
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(stock.name, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
                          Text(stock.code, style: TextStyle(color: Colors.grey.shade600)),
                        ],
                      ),
                      IconButton(
                        icon: Icon(_favoriteStocks.contains(stock) ? Icons.star : Icons.star_border, color: Colors.amber),
                        onPressed: () {
                          setState(() {
                            if (_favoriteStocks.contains(stock)) {
                              _favoriteStocks.remove(stock);
                            } else {
                              _favoriteStocks.add(stock);
                            }
                          });
                        },
                      ),
                    ],
                  ),
                  const SizedBox(height: 20),
                  SizedBox(height: 200, child: CustomPaint(size: const Size(double.infinity, 200), painter: DetailChartPainter(_generateChartData()))),
                  const SizedBox(height: 20),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceAround,
                    children: [
                      _buildDetailItem('最新价', stock.price.toStringAsFixed(2)),
                      _buildDetailItem('涨跌幅', '${stock.change}%'),
                      _buildDetailItem('成交量', stock.volume),
                    ],
                  ),
                ],
              ),
            );
          },
        );
      },
    );
  }

  Widget _buildDetailItem(String label, String value) {
    return Column(
      children: [
        Text(label, style: TextStyle(color: Colors.grey.shade600, fontSize: 12)),
        const SizedBox(height: 4),
        Text(value, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
      ],
    );
  }

  void _showSearch() {
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text('搜索股票'),
          content: TextField(controller: _searchController, decoration: const InputDecoration(hintText: '输入股票代码或名称'), autofocus: true),
          actions: [
            TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
            ElevatedButton(onPressed: () => Navigator.pop(context), child: const Text('搜索')),
          ],
        );
      },
    );
  }
}

class MiniChartPainter extends CustomPainter {
  final List<double> data;
  final Color color;

  MiniChartPainter(this.data, this.color);

  
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..color = color..strokeWidth = 1.5..style = PaintingStyle.stroke;
    final path = Path();
    for (int i = 0; i < data.length; i++) {
      final x = (i / (data.length - 1)) * size.width;
      final y = size.height - ((data[i] - data.reduce((a, b) => a < b ? a : b)) / (data.reduce((a, b) => a > b ? a : b) - data.reduce((a, b) => a < b ? a : b))) * size.height;
      if (i == 0) path.moveTo(x, y);
      else path.lineTo(x, y);
    }
    canvas.drawPath(path, paint);
  }

  
  bool shouldRepaint(covariant MiniChartPainter oldDelegate) => data != oldDelegate.data;
}

class DetailChartPainter extends CustomPainter {
  final List<double> data;

  DetailChartPainter(this.data);

  
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..color = Colors.red..strokeWidth = 2..style = PaintingStyle.stroke;
    final fillPaint = Paint()..shader = LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.red.withOpacity(0.3), Colors.red.withOpacity(0.05)]).createShader(Rect.fromLTWH(0, 0, size.width, size.height));

    final maxVal = data.reduce((a, b) => a > b ? a : b);
    final minVal = data.reduce((a, b) => a < b ? a : b);
    final range = maxVal - minVal;

    final path = Path();
    final fillPath = Path();

    for (int i = 0; i < data.length; i++) {
      final x = (i / (data.length - 1)) * size.width;
      final y = size.height - ((data[i] - minVal) / range) * size.height * 0.8 - size.height * 0.1;
      if (i == 0) {
        path.moveTo(x, y);
        fillPath.moveTo(x, size.height);
        fillPath.lineTo(x, y);
      } else {
        path.lineTo(x, y);
        fillPath.lineTo(x, y);
      }
    }
    fillPath.lineTo(size.width, size.height);
    fillPath.close();

    canvas.drawPath(fillPath, fillPaint);
    canvas.drawPath(path, paint);
  }

  
  bool shouldRepaint(covariant DetailChartPainter oldDelegate) => data != oldDelegate.data;
}

class StockInfo {
  final String code;
  final String name;
  final double price;
  final double change;
  final String volume;

  StockInfo({required this.code, required this.name, required this.price, required this.change, required this.volume});
}

5.2 核心功能解析

大盘指数展示

顶部展示上证指数、深证成指、创业板指三大指数,使用渐变背景突出显示。

股票列表

展示股票名称、代码、价格、涨跌幅和成交量,右侧显示迷你分时图。

自选股管理

支持添加和移除自选股,点击星星图标即可切换自选状态。

六、实际应用场景

6.1 日常投资

投资者可以随时查看股票行情,了解持仓股票的实时价格和涨跌情况。

6.2 市场分析

通过涨跌榜和热门股票,快速了解市场热点和资金流向。

6.3 决策支持

结合分时图表和成交量数据,为投资决策提供参考依据。

七、优化建议

7.1 实时行情推送

接入实时行情API,使用WebSocket实现行情数据实时推送。

7.2 K线图表

添加日K、周K、月K等K线图表,提供更丰富的技术分析工具。

7.3 交易功能

集成交易功能,支持直接在应用内进行买卖操作。

八、常见问题与解决方案

8.1 数据延迟

问题: 行情数据存在延迟,影响投资决策。

解决方案: 使用专业的行情数据服务商,确保数据的实时性和准确性。

8.2 自选股同步

问题: 更换设备后自选股列表丢失。

解决方案: 实现云端同步功能,将自选股数据保存到服务器。

九、总结

本文详细介绍了Flutter鸿蒙股票行情查看的实现方法,包括大盘指数、股票列表、分时图表、自选股管理等功能。通过本教程,开发者可以快速实现股票行情应用,为投资者提供便捷的行情查询服务。

十、参考资料

  • Flutter官方文档:https://flutter.dev
  • HarmonyOS开发者文档:https://developer.harmonyos.com
  • Flutter中国社区:https://flutter-io.cn
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐