I have an app, where user can select date (from/to), list of currencies, and then, display chart with some data. The chart, is just stacked bar chart, from MPAndroidChart. Recently, I've implemented SwiperRefreshLayout, so user can make more than just one api call. There's two cases of displaying chart:
First (everything is working fine) When user will select for example 2 currencies, he could make another calls, but he can't exceed firstly picked amount of currencies. So in this case, he can pick one, or two currencies. No matter which ones.
Second, program crashes User will pick X amount of currencies, the chart will be displayed, and after refreshing layout, user will pick X+1 currencies. This makes the program crash.
Since I've spend already a couple of days with this issue, I've tried to clear data, invalidating chart, etc. It turns out, that this command makes the program crash
mBinding.myChart.setCustom(myEntries)
I can run entire program without it, but it is needed, since it provides labels for chart.
Here's some code:
Global variables
private var myChartData: MutableList<BarEntry> = mutableListOf()
private var set: BarDataSet? = null
API call
private val apiCall: Job
get() =
viewLifecycleOwner.lifecycleScope.launch(start = CoroutineStart.LAZY) {
for (i in 0 until pickedCurrs.size) {
myPickedCurrencies += "${pickedCurrs[i]}, "
}
mViewModel.fetchData(
myBaseCurrency,
myPickedCurrencies,
fromDate,
toDate
)
mViewModel.myApiData.observe(viewLifecycleOwner, Observer { status ->
when (status) {
is MyWrapper.Success -> {
status.data?.timeSeriesRates?.entries?.forEachIndexed { index, entry ->
myChartData.add(
BarEntry(
index.toFloat(),
entry.value.values.map { it.toFloat() }.toFloatArray()
)
)
}
makeChart(
myChartData,
formatDate(status.data?.timeSeriesRates?.keys!!.toList()),
mSelectedCurrencies
)
}
is MyWrapper.Error -> {
Log.i(TAG, "onCreateView: ERROR")
}
}
})
}
makeChart
private fun prepareChart(
entries: MutableList<BarEntry>,
xAxisValues: ArrayList<String>,
pickedCurrs: MutableList<String>
) {
val colorsList = mutableListOf<Int>()
val legEn = mutableListOf<LegendEntry>()
for (i in pickedCurrs.indices) {
val color =
Color.argb(
255,
Random().nextInt(256),
Random().nextInt(256),
Random().nextInt(256)
)
if (!colorsList.contains(color)) {
colorsList.add(color)
}
}
set = BarDataSet(myEntries, "x")
set?.colors = colorsList
set?.highLightAlpha = 0
val data = BarData(set)
mBinding.timeSeriesChart.data = data
mBinding.timeSeriesChart.description?.isEnabled = false
mBinding.timeSeriesChart.setVisibleXRangeMaximum(6f)
mBinding.timeSeriesChart.barData?.setValueTextSize(12f)
mBinding.timeSeriesChart.xAxis?.position = XAxis.XAxisPosition.BOTTOM
mBinding.timeSeriesChart.setExtraOffsets(0f, 0f, 0f, 15f)
//
// //xAxis
mBinding.timeSeriesChart.xAxis?.setDrawLabels(true)
mBinding.timeSeriesChart.xAxis?.position = XAxis.XAxisPosition.BOTTOM_INSIDE
mBinding.timeSeriesChart.xAxis?.granularity = 1f
mBinding.timeSeriesChart.xAxis?.textSize = 11.5f
mBinding.timeSeriesChart.xAxis?.valueFormatter = IndexAxisValueFormatter(myXAxisValues)
mBinding.timeSeriesChart.xAxis?.labelRotationAngle = -20f
mBinding.timeSeriesChart.axisRight?.setDrawGridLines(false)
mBinding.timeSeriesChart.axisLeft.isEnabled = false
mBinding.timeSeriesChart.axisRight.isEnabled = false
//
// //legend
val legend = mBinding.timeSeriesChart.legend
legend?.verticalAlignment = Legend.LegendVerticalAlignment.BOTTOM
legend?.horizontalAlignment = Legend.LegendHorizontalAlignment.LEFT
legend?.orientation = Legend.LegendOrientation.HORIZONTAL
legend?.setDrawInside(false)
for (i in pickedCurrs.indices) {
legEn.add(
LegendEntry(
pickedCurrs[i],
Legend.LegendForm.SQUARE,
15f,
15f,
null,
colorsList[i]
)
)
}
legend?.setCustom(legEn)
set?.valueFormatter = object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return String.format("%2.02f", value)
}
}
mBinding.timeSeriesChart.invalidate()
mBinding.timeSeriesChart.refreshDrawableState()
}
EDIT stack trace
Process: com.example.x, PID: 15091
java.lang.IndexOutOfBoundsException: Index 1 out of bounds for length 1
at jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
at jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70)
at jdk.internal.util.Preconditions.checkIndex(Preconditions.java:266)
at java.util.Objects.checkIndex(Objects.java:359)
at java.util.ArrayList.get(ArrayList.java:434)
at com.github.mikephil.charting.renderer.LegendRenderer.renderLegend(LegendRenderer.java:377)
at com.github.mikephil.charting.charts.BarLineChartBase.onDraw(BarLineChartBase.java:281)
at android.view.View.draw(View.java:23889)
at android.view.View.updateDisplayListIfDirty(View.java:22756)
at android.view.View.draw(View.java:23620)
at android.view.ViewGroup.drawChild(ViewGroup.java:4556)
at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4317)
at androidx.constraintlayout.widget.ConstraintLayout.dispatchDraw(ConstraintLayout.java:1994)
at android.view.View.updateDisplayListIfDirty(View.java:22747)
at android.view.View.draw(View.java:23620)
at android.view.ViewGroup.drawChild(ViewGroup.java:4556)
at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4317)
at android.view.View.draw(View.java:23892)
at android.view.View.updateDisplayListIfDirty(View.java:22756)
at android.view.View.draw(View.java:23620)
at android.view.ViewGroup.drawChild(ViewGroup.java:4556)
at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4317)
at androidx.constraintlayout.widget.ConstraintLayout.dispatchDraw(ConstraintLayout.java:1994)
at android.view.View.updateDisplayListIfDirty(View.java:22747)
at android.view.View.draw(View.java:23620)
at android.view.ViewGroup.drawChild(ViewGroup.java:4556)
at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4317)
at android.view.View.updateDisplayListIfDirty(View.java:22747)
at android.view.View.draw(View.java:23620)
at android.view.ViewGroup.drawChild(ViewGroup.java:4556)
at androidx.recyclerview.widget.RecyclerView.drawChild(RecyclerView.java:5545)
at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4317)
at android.view.View.draw(View.java:23892)
at androidx.recyclerview.widget.RecyclerView.draw(RecyclerView.java:4944)
at android.view.View.updateDisplayListIfDirty(View.java:22756)
at android.view.View.draw(View.java:23620)
at android.view.ViewGroup.drawChild(ViewGroup.java:4556)
at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4317)
at android.view.View.updateDisplayListIfDirty(View.java:22747)
at android.view.View.draw(View.java:23620)
at android.view.ViewGroup.drawChild(ViewGroup.java:4556)
at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4317)
at android.view.View.updateDisplayListIfDirty(View.java:22747)
at android.view.View.draw(View.java:23620)
at android.view.ViewGroup.drawChild(ViewGroup.java:4556)
at androidx.fragment.app.FragmentContainerView.drawChild(FragmentContainerView.kt:235)
at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4317)
at androidx.fragment.app.FragmentContainerView.dispatchDraw(FragmentContainerView.kt:225)
at android.view.View.updateDisplayListIfDirty(View.java:22747)
at android.view.View.draw(View.java:23620)
at android.view.ViewGroup.drawChild(ViewGroup.java:4556)
at androidx.fragment.app.FragmentContainerView.drawChild(FragmentContainerView.kt:235)
at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4317)
at androidx.fragment.app.FragmentContainerView.dispatchDraw(FragmentContainerView.kt:225)
at android.view.View.updateDisplayListIfDirty(View.java:22747)
at android.view.View.draw(View.java:23620)
at android.view.ViewGroup.drawChild(ViewGroup.java:4556)
at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4317)
at android.view.View.updateDisplayListIfDirty(View.java:22747)
at android.view.View.draw(View.java:23620)
at android.view.ViewGroup.drawChild(ViewGroup.java:4556)
at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4317)
TL;DR Call
chart.notifyDataSetChanged()after callinglegend.setCustom(entries)Unfortunately I think this is a bug in MPAndroidChart. It will hit that crash if you increase the number of entries in a custom legend with notifying the chart that the dataset changed. I made a simple example that replicates this issue to test the fix.
If you run this demo problem and click the "Add Entry" button, it will crash with the same error you saw.
The reason is that the array of calculated legend text widths in the Legend object (
calculatedLabelSizes) isn't automatically updated to the new increased number of entries before the legend is rendered, leading to an IndexOutOfBoundsException in the LegendRenderer. The fix is to callnotifyDataSetChangedon the chart to force it to re-measure the legend and resize that internal array to match the number of legend entries before it is rendered, like thisDoing this, the demo works fine and new legend entries can be added without an issue.
Note
Your
if (!colorsList.contains(color)) {clause also has the potential to cause your app to crash. If your random color generator ever hits a duplicate, thecolorsListlength will be too short. This is pretty unlikely, but not impossible.