You need to enable JavaScript to run this app. Android如何基于Retrofit与MPAndroidChart实现MySQL小时更新数据绘图
优惠活动
大模型
产品
解决方案
定价
更多

Android如何基于Retrofit与MPAndroidChart实现MySQL小时更新数据绘图

问题描述

我参考教程实现了基于Retrofit的Android柱状图功能,使用静态JSON数据时可以完全复现教程的图表绘制效果。现在需要从MySQL拉取每小时更新的业务数据,之前参考过每小时定时执行的方案,计划通过按钮触发定时拉取,但不确定这种方式效率如何。
核心疑问:针对每小时更新的MySQL表/数据集,如何实现数据拉取,在完整保留历史数据值的前提下完成新数据的图表绘制?

已完成的代码

1. ApiClient请求配置类

public class ApiClient {
    private static Retrofit retrofit = null;

    private static OkHttpClient buildClient() {
        return new OkHttpClient
                .Builder()
                .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
                .build();
    }

    public static Retrofit getClient() {
        if (retrofit == null) {
            retrofit = new Retrofit.Builder()
                    .client(buildClient())
                    .addConverterFactory(GsonConverterFactory.create())
                    // API基础请求地址
                    .baseUrl("https://xxx") // 从MySQL拉取数据
                    .build();
        }
        return retrofit;
    }
}

2. 主页面图表实现类

public class BarchartRetrofit extends AppCompatActivity {
    public ApiInterface apiInterface;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        apiInterface = ApiClient.getClient().create(ApiInterface.class);
        // 调用接口获取服务端数据
        getData();
    }

    private void getData() {
        Call<PiChart> piChartCall = apiInterface.init();
        piChartCall.enqueue(new Callback<PiChart>() {
            @Override
            public void onResponse(Call<PiChart> call, Response<PiChart> response) {
                Log.d("CHART_RESPONSE", "" + response.body().getBarMonths().toString());
                setData(response.body());
            }

            @Override
            public void onFailure(Call<PiChart> call, Throwable t) {

            }
        });
    }

    private void setData(PiChart piChart) {
        ArrayList<BarDataSet> dataSets = null;

        ArrayList<BarEntry> completed = new ArrayList<>();
        for (int i = 0; i < piChart.getBarCompleted().size(); i++) {
            BarEntry value = new BarEntry(piChart.getBarCompleted().get(i), i);
            completed.add(value);
        }
        BarDataSet completeData = new BarDataSet(completed, "Completed Issue");
        completeData.setColor(Color.rgb(0, 155, 0));

        ArrayList<BarEntry> pending = new ArrayList<>();
        for (int i = 0; i < piChart.getBarCompleted().size(); i++) {
            BarEntry value = new BarEntry(piChart.getBarPending().get(i), i);
            pending.add(value);
        }
        BarDataSet pendingdata = new BarDataSet(pending, "Pending Issue");
        pendingdata.setColor(Color.rgb(253, 129, 0));

        ArrayList<BarEntry> rejected = new ArrayList<>();
        for (int i = 0; i < piChart.getBarCompleted().size(); i++) {
            BarEntry value = new BarEntry(piChart.getBarRejected().get(i), i);
            rejected.add(value);
        }
        BarDataSet rejectedData = new BarDataSet(rejected, "Rejected Issue");
        pendingdata.setColor(Color.rgb(255, 0, 0));

        dataSets = new ArrayList<>();
        dataSets.add(completeData);
        dataSets.add(pendingdata);
        dataSets.add(rejectedData);
        
        ArrayList<String> xAxis = new ArrayList<>();
        for (String months : piChart.getBarMonths()) {
            Log.d("CHART_RESPONSE", "month: " + months.toString());
            xAxis.add(months);
        }

        com.github.mikephil.charting.charts.BarChart chart = (com.github.mikephil.charting.charts.BarChart) findViewById(R.id.barchart);
        BarData data = new BarData(xAxis, dataSets);
        
        chart.setData(data);
        chart.setDescription("Bar chart");
        chart.invalidate();
    }
}

3. JSON实体类(POJO)

public class Example {
    @SerializedName("success")
    @Expose
    private boolean success;
    @SerializedName("bar_months")
    @Expose
    private List<String> barMonths = null;
    @SerializedName("bar_pending")
    @Expose
    private List<Integer> barPending = null;
    @SerializedName("bar_rejected")
    @Expose
    private List<Integer> barRejected = null;
    @SerializedName("bar_completed")
    @Expose
    private List<Integer> barCompleted = null;

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public List<String> getBarMonths() {
        return barMonths;
    }

    public void setBarMonths(List<String> barMonths) {
        this.barMonths = barMonths;
    }

    public List<Integer> getBarPending() {
        return barPending;
    }

    public void setBarPending(List<Integer> barPending) {
        this.barPending = barPending;
    }

    public List<Integer> getBarRejected() {
        return barRejected;
    }

    public void setBarRejected(List<Integer> barRejected) {
        this.barRejected = barRejected;
    }

    public List<Integer> getBarCompleted() {
        return barCompleted;
    }

    public void setBarCompleted(List<Integer> barCompleted) {
        this.barCompleted = barCompleted;
    }
}

接口返回JSON示例

{
  "success": true,
  "bar_months": [
    "jan",
    "feb",
    "mar"
  ],
  "bar_pending": [
    100,
    200,
    300
  ],
  "bar_rejected": [
    140,
    220,
    340
  ],
  "bar_completed": [
    170,
    290,
    310
  ]
}

注:接口返回的bar_months、bar_pending、bar_rejected、bar_completed对应的值并非固定静态值,会随业务持续动态更新,需要方案支持每小时拉取动态数据的同时保留历史值,完成图表正确绘制。


实现方案

一、定时拉取的效率优化

不要用按钮触发无限循环的定时器,这种方式如果页面销毁后没做清理会造成内存泄漏,且后台运行受系统限制。推荐用以下两种更高效的方案:

  • 页面可见时用WorkManager做周期任务,设置1小时的执行间隔,自动适配系统的休眠、省电策略,不需要手动绑定按钮点击状态
  • 如果需要手动控制启停,用Handler配合Runnable实现定时,同时在onDestroy、onPause生命周期里移除回调,避免泄漏
    注意:不建议用Timer,在Android上容易受Doze模式影响导致执行不准

二、历史数据保留的实现

你现在的代码逻辑是每次拿到新接口返回值就全量替换图表数据,只要改成本地持久化全量历史数据,每次拉取后合并即可,不需要改现有接口结构:

步骤1:本地缓存全量数据

数据量小的场景用SharedPreferences序列化存储,数据量大的场景用Room数据库,把每次接口返回的全量Example对象保存到本地,作为历史数据的唯一来源。

只要确认后端逻辑:MySQL每小时更新的是最新时间点的统计值,接口返回的是从业务开始到当前的全量时间序列,而不是只返回最新1小时的值,那你直接用接口返回的最新全量值渲染即可,不需要做额外合并。

步骤2:增量数据合并逻辑(如果接口只返回最新1小时数据)

如果后端为了省流量,每小时接口只返回最新1小时的统计值,按以下逻辑合并本地旧数据:

  1. 拉取到新数据后,和本地缓存的旧数据做比对:如果新的bar_months条目已经存在,就更新对应的统计值;如果不存在,就追加到列表末尾
  2. 合并完成后更新本地缓存,最后把合并后的全量数据集传给图表渲染
    示例合并逻辑代码片段
// 从本地缓存读出的全量历史数据
Example cachedOldData = getCachedDataFromLocal();
// 接口刚返回的最新增量数据
Example newIncrementData = response.body();

// 遍历新返回的月份数据,合并到缓存
for (int i = 0; i < newIncrementData.getBarMonths().size(); i++) {
    String monthKey = newIncrementData.getBarMonths().get(i);
    // 查本地缓存里有没有这个月份
    int existIndex = cachedOldData.getBarMonths().indexOf(monthKey);
    if (existIndex != -1) {
        // 存在就更新对应的值
        cachedOldData.getBarPending().set(existIndex, newIncrementData.getBarPending().get(i));
        cachedOldData.getBarRejected().set(existIndex, newIncrementData.getBarRejected().get(i));
        cachedOldData.getBarCompleted().set(existIndex, newIncrementData.getBarCompleted().get(i));
    } else {
        // 不存在就追加
        cachedOldData.getBarMonths().add(monthKey);
        cachedOldData.getBarPending().add(newIncrementData.getBarPending().get(i));
        cachedOldData.getBarRejected().add(newIncrementData.getBarRejected().get(i));
        cachedOldData.getBarCompleted().add(newIncrementData.getBarCompleted().get(i));
    }
}
// 合并完更新本地缓存
saveDataToLocal(cachedOldData);
// 用合并后的全量数据渲染图表
setData(cachedOldData);

步骤3:现有代码的修正

你现在的代码有个小bug:rejectedData的颜色设置错了,写成了给pendingdata设红色,需要修正为:

// 原错误代码
// pendingdata.setColor(Color.rgb(255, 0, 0));
// 修正为
rejectedData.setColor(Color.rgb(255, 0, 0));

另外,每次更新数据的时候不要重新new整个BarData对象,直接调用chart.getData()对应的方法更新数据集,再触发chart.notifyDataSetChanged()、chart.invalidate()即可,减少视图重绘的开销。


内容的提问来源于stack exchange,提问作者swiftlearneer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.11.06 18:45:05