Flutter:K线图

使用插件:k_chart

k_chart: ^0.7.1

在这里插入图片描述
在这里插入图片描述

首页进入详情需要传对应币种的symbol:BTCUSDT

.onTap((){
    
    
	Get.toNamed('/k_chart_page', arguments: {
    
    'symbol': item.symbol});
}),

下面主要记录下K线的实现过程

1、定义model模型,前端需要传给后台的

{
    
    
	symbol: BTCUSDT, // 币种
	interval: 1m, // 时间
	limit: 500, // 条数
}

import 'package:xiaoshukeji/common/index.dart';
class HomeKlineReq {
    
    
	String? symbol;
	String? interval;
	int? limit;

	HomeKlineReq({
    
    this.symbol, this.interval, this.limit});

	factory HomeKlineReq.fromJson(Map<String, dynamic> json) => HomeKlineReq(
				symbol: DataUtils.toStr(json['symbol']),
				interval: DataUtils.toStr(json['interval']),
				limit: DataUtils.toInt(json['limit']),
			);

	Map<String, dynamic> toJson() => {
    
    
				'symbol': symbol,
				'interval': interval,
				'limit': limit,
			};
}

2、后台返回数据格式处理,数据来源于火币网

[
  [
    1499040000000,      // 开盘时间
    "0.01634790",       // 开盘价
    "0.80000000",       // 最高价
    "0.01575800",       // 最低价
    "0.01577100",       // 收盘价(当前K线未结束的即为最新价)
    "148976.11427815",  // 成交量
    1499644799999,      // 收盘时间
    "2434.19055334",    // 成交额
    308,                // 成交笔数
    "1756.87402397",    // 主动买入成交量
    "28.46694368",      // 主动买入成交额
    "17928899.62484339" // 请忽略该参数
  ]
]

这个格式并不能直接在k_chart中使用,需要自己处理下数据
在flutter_k_chart演示demo中,找到了官方的数据默认格式

// kline_1M.json id=时间戳
[{
    
    
        "amount": 46197.237447628898264056,
        "open": 7121.890000000000000000,
        "close": 7102.900000000000000000,
        "high": 7380.000000000000000000,
        "id": 1574438400,
        "count": 296348,
        "low": 7094.690000000000000000,
        "vol": 333578883.234124950380397343870000000000000000
    },
    {
    
    
        "id": 1574352000,
        "open": 7628.590000000000000000,
        "close": 7121.440000000000000000,
        "high": 7725.090000000000000000,
        "low": 6790.000000000000000000,
        "vol": 664521165.622325248991851131,
        "amount": 91327.402878725150140416,
        "count": 627969
    }
]

按照这个格式制作model

import 'package:xiaoshukeji/common/index.dart';

class KlineData {
    
    
  final double amount;
  final double open;
  final double close;
  final double high;
  final int time;
  final int count;
  final double low;
  final double vol;

  KlineData({
    
    
    required this.amount,
    required this.open,
    required this.close,
    required this.high,
    required this.time,
    required this.count,
    required this.low,
    required this.vol,
  });

  factory KlineData.fromJson(List<dynamic> json) {
    
    
    return KlineData(
      amount: DataUtils.toDouble(json[5]) ?? 0, // 成交量
      open: DataUtils.toDouble(json[1]) ?? 0, // 开盘价
      close: DataUtils.toDouble(json[4]) ?? 0, // 收盘价
      high: DataUtils.toDouble(json[2]) ?? 0, // 最高价
      time: DataUtils.toInt(json[0]) ?? 0, // 时间
      count: DataUtils.toInt(json[8]) ?? 0, // 笔数
      low: DataUtils.toDouble(json[3]) ?? 0, // 最低价
      vol: DataUtils.toDouble(json[7]) ?? 0, // 24小时成交量
    );
  }

  Map<String, dynamic> toJson() => {
    
    
        'amount': amount,
        'open': open,
        'close': close,
        'high': high,
        'time': time,
        'count': count,
        'low': low,
        'vol': vol,
      };
}

// 接口请求,获取K线数据
  static Future<List<KlineData>> klineData(HomeKlineReq data) async {
    
    
    var res = await WPHttpService.to.get(
      '/api/markets/api/v3/klines',
      params: data.toJson(),
    );
    var klineData = res.data;
    if (klineData != null && klineData is List && klineData.isNotEmpty) {
    
    
      return klineData.map((e) => KlineData.fromJson(e)).toList();
    }
    return [];
  }

3、controller控制器获取数据

import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:k_chart/flutter_k_chart.dart';
import 'dart:async';

import 'package:xiaoshukeji/common/index.dart';

class KChartController extends GetxController {
    
    
  KChartController();

  // 币种
  String symbol = '';
  // 头部24小时数据
  HomeKlineHeadPriceModel headPrice = HomeKlineHeadPriceModel();
  // K线数据
  List<KLineEntity> datas = []; // 初始化为空列表
  // 主图指标
  MainState mainState = MainState.MA;
  // 副图指标
  SecondaryState secondaryState = SecondaryState.MACD;
  // 当前选中的时间周期
  String period = '1m'; // 默认1分钟
  // 定时器
  Timer? _timer;

  //

  // 初始化数据
  @override
  void onInit() {
    
    
    super.onInit();
    _initData();
    _startTimer();
  }

  @override
  void onClose() {
    
    
    _timer?.cancel();
    super.onClose();
  }

  //

  // 初始化数据
  void _initData() async {
    
    
    symbol = Get.arguments?['symbol'] ?? '';
    fetchKLineHeadPrice();
    fetchKLineData();
  }

  // 获取K线头部24小时数据
  Future<void> fetchKLineHeadPrice() async {
    
    
    headPrice = await HomeApi.klineHeadPrice(symbol);
    update(["k_chart"]);
  }

  // 获取K线数据
  Future<void> fetchKLineData() async {
    
    
    try {
    
    
      // 根据不同周期读取不同的json文件
      List<KlineData> klineData = await HomeApi.klineData(HomeKlineReq(symbol: symbol, interval: period, limit: 1000));

      datas = klineData.map((item) {
    
    
        double open = item.open;
        double close = item.close;
        double change = close - open; // change: 价格变化,通常是收盘价与开盘价的差值
        double ratio = (change / open) * 100; // ratio: 价格变化百分比

        return KLineEntity.fromCustom(
          time: item.time,
          open: open,
          high: item.high,
          low: item.low,
          close: close,
          vol: item.vol,
          amount: item.amount,
          change: change,
          ratio: ratio,
        );
      }).toList();

      DataUtil.calculate(datas);
      update(["k_chart"]);
    } catch (e) {
    
    
      print('获取K线数据失败: $e');
    }
  }

  // 启动定时器
  void _startTimer() {
    
    
    _timer = Timer.periodic(const Duration(seconds: 5), (timer) {
    
    
      if (datas.isEmpty) return;
      fetchKLineData();
    });
  }

  // 切换时间周期
  void changePeriod(String newPeriod) {
    
    
    period = newPeriod;
    print('切换周期: $period');
    fetchKLineData(); // 重新获取对应周期的数据
    update(["k_chart"]);
  }

  // 切换主图指标
  void changeMainState(MainState state) {
    
    
    mainState = state;
    update(["k_chart"]);
  }

  // 切换副图指标
  void changeSecondaryState(SecondaryState state) {
    
    
    secondaryState = state;
    update(["k_chart"]);
  }

  // 获取价格变化颜色
  Color getPriceChangeColor(String? percent) {
    
    
    if (percent == null) return AppTheme.textColorlv;
    // 转换为 double 进行比较
    try {
    
    
      double value = double.parse(percent);
      return value < 0 ? const Color(0xffcd3746) : const Color(0xff01b56b);
    } catch (e) {
    
    
      return AppTheme.textColorlv; // 转换失败返回默认颜色
    }
  }
}

4、view视图

import 'package:ducafe_ui_core/ducafe_ui_core.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:k_chart/k_chart_widget.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
import 'package:xiaoshukeji/common/index.dart';
import 'package:k_chart/flutter_k_chart.dart';
import 'index.dart';

class KChartPage extends GetView<KChartController> {
    
    
  const KChartPage({
    
    super.key});

  // 头部数据
  Widget _buildHeader() {
    
    
    return <Widget>[
      <Widget>[
        TextWidget.body('${controller.headPrice.lastPrice}',size:30.sp,color: AppTheme.textColorfff,weight: FontWeight.w600,),
        SizedBox(height: 15.w),
        <Widget>[
          TextWidget.body('${controller.headPrice.priceChangePercent}%',size:26.sp,color: AppTheme.textColorfff,weight: FontWeight.w600,),
        ].toRow().paddingHorizontal(30.w).card(
          color: controller.getPriceChangeColor(controller.headPrice.priceChangePercent),
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.w)),
        ).height(40.w)
      ].toColumn(mainAxisAlignment: MainAxisAlignment.center),
      <Widget>[
        TextWidget.body('24h量 ${controller.headPrice.volume}',size:24.sp,color: AppTheme.textColorfff,),
        SizedBox(height: 10.w),
        TextWidget.body('24h高 ${controller.headPrice.highPrice}',size:24.sp,color: AppTheme.textColorfff,),
        SizedBox(height: 10.w),
        TextWidget.body('24h低 ${controller.headPrice.lowPrice}',size:24.sp,color: AppTheme.textColorfff,),
      ].toColumn(mainAxisAlignment: MainAxisAlignment.center,crossAxisAlignment: CrossAxisAlignment.start),
    ].toRow(mainAxisAlignment: MainAxisAlignment.spaceBetween).paddingHorizontal(30.w).card(color: AppTheme.blockBgColor).tight(width: 690.w, height: 160.w);
  }

  // 时间选择
  Widget _buildPeriodButtons() {
    
    
    return <Widget>[
      _buildPeriodButton('1m', '1分钟'),
      _buildPeriodButton('5m', '5分钟'),
      _buildPeriodButton('15m', '15分钟'),
      _buildPeriodButton('30m', '30分钟'),
      _buildPeriodButton('1h', '1小时'),
      _buildPeriodButton('1d', '日线'),
    ].toRow(mainAxisAlignment: MainAxisAlignment.spaceBetween).paddingHorizontal(30.w).height(90.w);
  }
  Widget _buildPeriodButton(String period, String label) {
    
    
    bool isSelected = controller.period == period;
    return TextWidget.body(label, color: isSelected ? AppTheme.primaryYellow : AppTheme.textColor646).onTap(() {
    
    
      controller.changePeriod(period);
    });
  }

  // 指标切换按钮
  Widget _buildIndicatorButtons() {
    
    
    return <Widget>[
      TextWidget.body('MA', color: controller.mainState == MainState.MA ? AppTheme.primaryYellow : AppTheme.textColor646).onTap(() {
    
    
        controller.changeMainState(MainState.MA);
      }),
      SizedBox(width: 20.w),
      TextWidget.body('BOLL',color: controller.mainState == MainState.BOLL ? AppTheme.primaryYellow : AppTheme.textColor646).onTap(() {
    
    
        controller.changeMainState(MainState.BOLL);
      }),
      SizedBox(width: 100.w),
      TextWidget.body('MACD',color: controller.secondaryState == SecondaryState.MACD ? AppTheme.primaryYellow : AppTheme.textColor646).onTap(() {
    
    
        controller.changeSecondaryState(SecondaryState.MACD);
      }),
      SizedBox(width: 20.w),
      TextWidget.body('KDJ',color: controller.secondaryState == SecondaryState.KDJ ? AppTheme.primaryYellow : AppTheme.textColor646 ).onTap(() {
    
    
        controller.changeSecondaryState(SecondaryState.KDJ);
      }),
      SizedBox(width: 20.w),
      TextWidget.body('RSI',color: controller.secondaryState == SecondaryState.RSI ? AppTheme.primaryYellow : AppTheme.textColor646).onTap(() {
    
    
        controller.changeSecondaryState(SecondaryState.RSI);
      }),
    ].toRow(mainAxisAlignment: MainAxisAlignment.spaceBetween).paddingHorizontal(60.w).card(color: AppTheme.blockBgColor).height(80.w);
  }

  @override
  Widget build(BuildContext context) {
    
    
    return GetBuilder<KChartController>(
      init: KChartController(),
      id: "k_chart",
      builder: (_) {
    
    
        return Scaffold(
          backgroundColor: AppTheme.pageBgColor,
          appBar: TDNavBar(
            height: 45,
            title: controller.symbol,
            titleColor: AppTheme.textColorfff,
            titleFontWeight: FontWeight.w600,
            backgroundColor: AppTheme.navBarBgColor,
            screenAdaptation: true,
            useDefaultBack: true,
          ),
          body: Column(
            children: [
              SizedBox(height: 30.w),
              _buildHeader(),
              _buildPeriodButtons(),
              SizedBox(
                width: 750.w,
                child: KChartWidget(
                  controller.datas,
                  ChartStyle(),
                  ChartColors(),
                  isLine: false,
                  isTapShowInfoDialog:true, // 单击显示详细信息
                  mainState: controller.mainState,
                  secondaryState: controller.secondaryState,
                  volHidden: true, // 隐藏量
                  isTrendLine: false, // 显示趋势线
                ),
              ).expanded(),
              _buildIndicatorButtons(),
            ],
          ),
        );
      },
    );
  }
}