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小时的统计值,按以下逻辑合并本地旧数据:
- 拉取到新数据后,和本地缓存的旧数据做比对:如果新的
bar_months条目已经存在,就更新对应的统计值;如果不存在,就追加到列表末尾 - 合并完成后更新本地缓存,最后把合并后的全量数据集传给图表渲染
示例合并逻辑代码片段
// 从本地缓存读出的全量历史数据 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

