【问题标题】:MPAndroid Chart dissapears after calling invalidate() with new dataMP Android Chart 在使用新数据调用 invalidate() 后消失
【发布时间】:2020-05-27 13:31:44
【问题描述】:

在我的天气应用程序中,我有一个 MainFragment,它有一个打开不同片段 (SearchFragment)(通过替换)的按钮,允许用户选择一个位置,然后获取该位置的天气数据并将其加载到各种视图中包括一个 MPAndroid LineChart。我的问题是,每当我从搜索片段返回时,虽然为图表获取了新数据并且我正在调用 chart.notifyDataSetChanged()chart.invalidate()(也尝试了 chart.postInvalidate(),因为在处理另一个线程时建议使用它)在调用 invalidate() 之后,图表就会消失。我在这里错过了什么?

主片段:

const val UNIT_SYSTEM_KEY = "UNIT_SYSTEM"
const val LATEST_CURRENT_LOCATION_KEY = "LATEST_CURRENT_LOC"

class MainFragment : Fragment() {

// Lazy inject the view model
private val viewModel: WeatherViewModel by viewModel()
private lateinit var weatherUnitConverter: WeatherUnitConverter

private val TAG = MainFragment::class.java.simpleName

// View declarations
...

// OnClickListener to handle the current weather's "Details" layout expansion/collapse
private val onCurrentWeatherDetailsClicked = View.OnClickListener {
    if (detailsExpandedLayout.visibility == View.GONE) {
        detailsExpandedLayout.visibility = View.VISIBLE
        detailsExpandedArrow.setImageResource(R.drawable.ic_arrow_up_black)
    } else {
        detailsExpandedLayout.visibility = View.GONE
        detailsExpandedArrow.setImageResource(R.drawable.ic_down_arrow)
    }
}

// OnClickListener to handle place searching using the Places SDK
private val onPlaceSearchInitiated = View.OnClickListener {
    (activity as MainActivity).openSearchPage()
}

// RefreshListener to update the UI when the location settings are changed
private val refreshListener = SwipeRefreshLayout.OnRefreshListener {
    Toast.makeText(activity, "calling onRefresh()", Toast.LENGTH_SHORT).show()
    swipeRefreshLayout.isRefreshing = false
}

// OnClickListener to allow navigating from this fragment to the settings one
private val onSettingsButtonClicked: View.OnClickListener = View.OnClickListener {
    (activity as MainActivity).openSettingsPage()
}

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    val view = inflater.inflate(R.layout.main_fragment, container, false)
    // View initializations
    .....
    hourlyChart = view.findViewById(R.id.lc_hourly_forecasts)
    return view
}

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    setUpChart()
    lifecycleScope.launch {
        // Shows a lottie animation while the data is being loaded
        //scrollView.visibility = View.GONE
        //lottieAnimView.visibility = View.VISIBLE
        bindUIAsync().await()
        // Stops the animation and reveals the layout with the data loaded
        //scrollView.visibility = View.VISIBLE
        //lottieAnimView.visibility = View.GONE
    }
}



@SuppressLint("SimpleDateFormat")
    private fun bindUIAsync() = lifecycleScope.async(Dispatchers.Main) {
        // fetch current weather
        val currentWeather = viewModel.currentWeatherData

    // Observe the current weather live data
    currentWeather.observe(viewLifecycleOwner, Observer { currentlyLiveData ->
        if (currentlyLiveData == null) return@Observer

        currentlyLiveData.observe(viewLifecycleOwner, Observer { currently ->

            setCurrentWeatherDate(currently.time.toDouble())

            // Get the unit system pref's value
            val unitSystem = viewModel.preferences.getString(
                UNIT_SYSTEM_KEY,
                UnitSystem.SI.name.toLowerCase(Locale.ROOT)
            )

            // set up views dependent on the Unit System pref's value
            when (unitSystem) {
                UnitSystem.SI.name.toLowerCase(Locale.ROOT) -> {
                    setCurrentWeatherTemp(currently.temperature)
                    setUnitSystemImgView(unitSystem)
                }
                UnitSystem.US.name.toLowerCase(Locale.ROOT) -> {
                    setCurrentWeatherTemp(
                        weatherUnitConverter.convertToFahrenheit(
                            currently.temperature
                        )
                    )
                    setUnitSystemImgView(unitSystem)
                }
            }

            setCurrentWeatherSummaryText(currently.summary)
            setCurrentWeatherSummaryIcon(currently.icon)
            setCurrentWeatherPrecipProb(currently.precipProbability)
        })
    })

    // fetch the location
    val weatherLocation = viewModel.weatherLocation
    // Observe the location for changes
    weatherLocation.observe(viewLifecycleOwner, Observer { locationLiveData ->
        if (locationLiveData == null) return@Observer

        locationLiveData.observe(viewLifecycleOwner, Observer { location ->
            Log.d(TAG,"location update = $location")
            locationTxtView.text = location.name
        })
    })

    // fetch hourly weather
    val hourlyWeather = viewModel.hourlyWeatherEntries

    // Observe the hourly weather live data
    hourlyWeather.observe(viewLifecycleOwner, Observer { hourlyLiveData ->
        if (hourlyLiveData == null) return@Observer

        hourlyLiveData.observe(viewLifecycleOwner, Observer { hourly ->
            val xAxisLabels = arrayListOf<String>()
            val sdf = SimpleDateFormat("HH")
            for (i in hourly.indices) {
                val formattedLabel = sdf.format(Date(hourly[i].time * 1000))
                xAxisLabels.add(formattedLabel)
            }
            setChartAxisLabels(xAxisLabels)
        })
    })

    // fetch weekly weather
    val weeklyWeather = viewModel.weeklyWeatherEntries

    // get the timezone from the prefs
    val tmz = viewModel.preferences.getString(LOCATION_TIMEZONE_KEY, "America/Los_Angeles")!!

    // observe the weekly weather live data
    weeklyWeather.observe(viewLifecycleOwner, Observer { weeklyLiveData ->
        if (weeklyLiveData == null) return@Observer

        weeklyLiveData.observe(viewLifecycleOwner, Observer { weatherEntries ->
            // update the recyclerView with the new data
            (weeklyForecastRCV.adapter as WeeklyWeatherAdapter).updateWeeklyWeatherData(
                weatherEntries, tmz
            )
            for (day in weatherEntries) { //TODO:sp replace this with the full list once the repo issue is fixed
                val zdtNow = Instant.now().atZone(ZoneId.of(tmz))
                val dayZdt = Instant.ofEpochSecond(day.time).atZone(ZoneId.of(tmz))
                val formatter = DateTimeFormatter.ofPattern("MM-dd-yyyy")
                val formattedNowZtd = zdtNow.format(formatter)
                val formattedDayZtd = dayZdt.format(formatter)
                if (formattedNowZtd == formattedDayZtd) { // find the right week day whose data we want to use for the UI
                    initTodayData(day, tmz)
                }
            }
        })
    })

    // get the hourly chart's computed data
    val hourlyChartLineData = viewModel.hourlyChartData

    // Observe the chart's data
    hourlyChartLineData.observe(viewLifecycleOwner, Observer { lineData ->
        if(lineData == null) return@Observer

        hourlyChart.data = lineData // Error due to the live data value being of type Unit
    })

    return@async true
}

...

private fun setChartAxisLabels(labels: ArrayList<String>) {
    // Populate the X axis with the hour labels
    hourlyChart.xAxis.valueFormatter = IndexAxisValueFormatter(labels)
}

/**
 * Sets up the chart with the appropriate
 * customizations.
 */
private fun setUpChart() {
    hourlyChart.apply {
        description.isEnabled = false
        setNoDataText("Data is loading...")

        // enable touch gestures
        setTouchEnabled(true)
        dragDecelerationFrictionCoef = 0.9f

        // enable dragging
        isDragEnabled = true
        isHighlightPerDragEnabled = true
        setDrawGridBackground(false)
        axisRight.setDrawLabels(false)
        axisLeft.setDrawLabels(false)
        axisLeft.setDrawGridLines(false)
        xAxis.setDrawGridLines(false)
        xAxis.isEnabled = true

        // disable zoom functionality
        setScaleEnabled(false)
        setPinchZoom(false)
        isDoubleTapToZoomEnabled = false

        // disable the chart's legend
        legend.isEnabled = false

        // append extra offsets to the chart's auto-calculated ones
        setExtraOffsets(0f, 0f, 0f, 10f)

        data = LineData()
        data.isHighlightEnabled = false
        setVisibleXRangeMaximum(6f)
        setBackgroundColor(resources.getColor(R.color.bright_White, null))
    }

    // X Axis setup
    hourlyChart.xAxis.apply {
        position = XAxis.XAxisPosition.BOTTOM
        textSize = 14f
        setDrawLabels(true)
        setDrawAxisLine(false)
        granularity = 1f // one hour
        spaceMax = 0.2f // add padding start
        spaceMin = 0.2f // add padding end
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            typeface = resources.getFont(R.font.work_sans)
        }
        textColor = resources.getColor(R.color.black, null)
    }

    // Left Y axis setup
    hourlyChart.axisLeft.apply {
        setDrawLabels(false)
        setDrawGridLines(false)
        setPosition(YAxis.YAxisLabelPosition.OUTSIDE_CHART)
        isEnabled = false
        isGranularityEnabled = true
        // temperature values range (higher than probable temps in order to scale down the chart)
        axisMinimum = 0f
        axisMaximum = when (getUnitSystemValue()) {
            UnitSystem.SI.name.toLowerCase(Locale.ROOT) -> 50f
            UnitSystem.US.name.toLowerCase(Locale.ROOT) -> 150f
            else -> 50f
        }
    }

    // Right Y axis setup
   hourlyChart.axisRight.apply {
       setDrawGridLines(false)
       isEnabled = false
   }
}
}

ViewModel 类:

class WeatherViewModel(
private val forecastRepository: ForecastRepository,
private val weatherUnitConverter: WeatherUnitConverter,
context: Context
) : ViewModel() {

private val appContext = context.applicationContext

// Retrieve the sharedPrefs
val preferences:SharedPreferences
    get() = PreferenceManager.getDefaultSharedPreferences(appContext)

// This will run only when currentWeatherData is called from the View
val currentWeatherData = liveData {
    val task = viewModelScope.async {  forecastRepository.getCurrentWeather() }
    emit(task.await())
}

val hourlyWeatherEntries = liveData {
    val task = viewModelScope.async {  forecastRepository.getHourlyWeather() }
    emit(task.await())
}

val weeklyWeatherEntries = liveData {
    val task = viewModelScope.async {
        val currentDateEpoch = LocalDate.now().toEpochDay()
        forecastRepository.getWeekDayWeatherList(currentDateEpoch)
    }
    emit(task.await())
}

val weatherLocation = liveData {
    val task = viewModelScope.async(Dispatchers.IO) {
        forecastRepository.getWeatherLocation()
    }
    emit(task.await())
}

val hourlyChartData = liveData {
    val task = viewModelScope.async(Dispatchers.Default) {
        // Build the chart data
        hourlyWeatherEntries.observeForever { hourlyWeatherLiveData ->
            if(hourlyWeatherLiveData == null) return@observeForever

            hourlyWeatherLiveData.observeForever {hourlyWeather ->
                createChartData(hourlyWeather)
            }
        }
    }
    emit(task.await())
}

/**
 * Creates the line chart's data and returns them.
 * @return The line chart's data (x,y) value pairs
 */
private fun createChartData(hourlyWeather: List<HourWeatherEntry>?): LineData {
    if(hourlyWeather == null) return LineData()

    val unitSystemValue = preferences.getString(UNIT_SYSTEM_KEY, "si")!!
    val values = arrayListOf<Entry>()

    for (i in hourlyWeather.indices) { // init data points
        // format the temperature appropriately based on the unit system selected
        val hourTempFormatted = when (unitSystemValue) {
            UnitSystem.SI.name.toLowerCase(Locale.ROOT) -> hourlyWeather[i].temperature
            UnitSystem.US.name.toLowerCase(Locale.ROOT) -> weatherUnitConverter.convertToFahrenheit(
                hourlyWeather[i].temperature
            )
            else -> hourlyWeather[i].temperature
        }

        // Create the data point
        values.add(
            Entry(
                i.toFloat(),
                hourTempFormatted.toFloat(),
                appContext.resources.getDrawable(determineSummaryIcon(hourlyWeather[i].icon), null)
            )
        )
    }
    Log.d("MainFragment viewModel", "$values")
    // create a data set and customize it
    val lineDataSet = LineDataSet(values, "")

    val color = appContext.resources.getColor(R.color.black, null)
    val offset = MPPointF.getInstance()
    offset.y = -35f

    lineDataSet.apply {
        valueFormatter = YValueFormatter()
        setDrawValues(true)
        fillDrawable = appContext.resources.getDrawable(R.drawable.gradient_night_chart, null)
        setDrawFilled(true)
        setDrawIcons(true)
        setCircleColor(color)
        mode = LineDataSet.Mode.HORIZONTAL_BEZIER
        this.color = color // line color
        iconsOffset = offset
        lineWidth = 3f
        valueTextSize = 9f
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            valueTypeface = appContext.resources.getFont(R.font.work_sans_medium)
        }
    }

    // create a LineData object using our LineDataSet
    val data = LineData(lineDataSet)
    data.apply {
        setValueTextColor(R.color.colorPrimary)
        setValueTextSize(15f)
    }
    return data
}

private fun determineSummaryIcon(icon: String): Int {
    return when (icon) {
        "clear-day" -> R.drawable.ic_sun
        "clear-night" -> R.drawable.ic_moon
        "rain" -> R.drawable.ic_precipitation
        "snow" -> R.drawable.ic_snowflake
        "sleet" -> R.drawable.ic_sleet
        "wind" -> R.drawable.ic_wind_speed
        "fog" -> R.drawable.ic_fog
        "cloudy" -> R.drawable.ic_cloud_coverage
        "partly-cloudy-day" -> R.drawable.ic_cloudy_day
        "partly-cloudy-night" -> R.drawable.ic_cloudy_night
        "hail" -> R.drawable.ic_hail
        "thunderstorm" -> R.drawable.ic_thunderstorm
        "tornado" -> R.drawable.ic_tornado
        else -> R.drawable.ic_sun
    }
}

}

延迟延迟:

fun<T> lazyDeferred(block: suspend CoroutineScope.() -> T) : Lazy<Deferred<T>> {
    return lazy {
        GlobalScope.async {
            block.invoke(this)
        }
    }
}

ScopedFragment:

abstract class ScopedFragment : Fragment(), CoroutineScope {
private lateinit var job: Job

override val coroutineContext: CoroutineContext
    get() = job + Dispatchers.Main

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    job = Job()
}

override fun onDestroy() {
    job.cancel()
    super.onDestroy()
}
}

【问题讨论】:

  • 能否请您发布VM的代码?这个问题可能与线程(在后台线程上更新 ui)或生命周期管理有关。返回后数据可以为空。
  • @AnisBENNSIR 当然,将它与 LazyDeferred 定义一起添加
  • ScopedFragment 看起来如何?
  • @SomerandomITboy 刚刚将其添加到帖子中 :)

标签: android kotlin mpandroidchart kotlin-coroutines


【解决方案1】:

如果没有整个环境,我很难帮助您调试整个事情,但我很高兴为您提供一些乍一看似乎有点不对劲的东西。

首先,我会避免自己管理所有 CoroutinesScopes 和生命周期,这很容易出错。所以我会依赖 Android 团队已经完成的工作。快速查看here,它非常易于设置和使用。开发体验很棒。

LiveData 上发布Deferred 并在视图侧等待看起来像代码味道......

  • 如果出现网络错误怎么办? 这将导致抛出异常或取消异常。

  • 如果任务已经执行并导致某种类型的 UI 一致性问题怎么办?这些是我不想处理的几个极端情况。

只需观察LiveData,因为它是它的主要目的:它是一个价值持有者,它旨在经历Fragment 中的几个生命周期事件。因此,一旦重新创建视图,ViewModel 内的 LiveData 中的值就会准备就绪。

您的lazyDeferred 函数非常聪明,但在Android 世界中它也很危险。这些协程并不存在于任何生命周期控制的范围内,因此它们很有可能最终被泄露。相信我,您不希望任何协程被泄露,因为即使在视图模型和片段破坏之后它们仍会继续工作,这是您绝对不想要的。

所有这些都可以通过使用我之前提到的依赖项轻松修复,我将 paste here once more

这是一个关于如何在 ViewModel 中使用这些实用程序来确保事物的生命周期和协程不会导致任何问题的 sn-p:

class WeatherViewModel(
    private val forecastRepository: ForecastRepository,
    context: Context
) : ViewModel() {

    private val appContext = context.applicationContext

    // Retrieve the sharedPrefs
    val preferences:SharedPreferences
        get() = PreferenceManager.getDefaultSharedPreferences(appContext)

    // This will run only when currentWeatherData is called from the View
    val currentWeatherData = liveData {
        val task = viewModelScope.async { forecastRepository.getCurrentWeather() }
        emit(task.await())
    }

    val hourlyWeatherEntries = liveData {
        val task = viewModelScope.async { forecastRepository.getHourlyWeather() }
        emit(task.await())

    }

    val weeklyWeatherEntries = liveData {
        val task = viewModelScope.async {
            val currentDateEpoch = LocalDate.now().toEpochDay()
            forecastRepository.getWeekDayWeatherList(currentDateEpoch)
        }
        emit(task.await())
    }

    val weatherLocation = liveData {
        val task = viewModelScope.async(Dispatchers.IO) {
            forecastRepository.getWeatherLocation()
        }
        emit(task.await())
    }

}

通过使用以下方法,所有网络调用都以并行方式执行,它们都与viewModelScope 绑定,而无需编写任何处理 CoroutineScope 生命周期的代码。当 ViewModel 消亡时,作用域也会消亡。当视图被重新创建时,例程不会执行两次并且值将准备好读取。

关于图表的配置:我强烈建议您在创建视图后立即配置图表,因为它高度相关。配置是你想要只做一次的事情MPAndroid 使用饼图。

关于图表的更多信息:构建LineData 的所有逻辑在后台线程上会更好,并通过 ViewModel 端的 LiveData 公开,就像你对所有其他人所做的那样

val property = liveData {
    val deferred = viewModelScope.async(Dispatchers.Default) {
        // Heavy building logic like:
        createChartData()
    }
    emit(deferred.await())
}

专业 Kotlin 提示:避免在那些冗长的 MPAndroid 配置函数中重复自己。

代替:

view.configureThis()
view.configureThat()
view.enabled = true

做:

view.apply {
    configureThis()
    configureThat()
    enabled = true
}

很遗憾,我只能给您这些提示,但无法准确指出您的问题是什么,因为该错误与运行时的整个生命周期演变过程中发生的事情密切相关,但希望这会有用

回答您的评论

如果您的一个数据流 (LiveData) 依赖于另一个数据流 (另一个 LiveData) 将要发出的内容,则您正在寻找 LiveData.mapLiveData.switchMap 操作。

我想hourlyWeatherEntries 会不时发出值。

在这种情况下,您可以使用LiveData.switchMap

这样做的作用是,每当 source LiveData 发出一个值时,您都会收到一个回调,并且您应该返回一个带有新值的新实时数据。 p>

你可以安排如下:

val hourlyChartData = hourlyWeatherEntries.switchMap { hourlyWeather ->
    liveData {
        val task = viewModelScope.async(Dispatchers.IO) {
            createChartData(hourlyWeather)
        }
        emit(task.await())
    }
}

使用这种方法的好处是它完全是懒惰的。这意味着不会发生计算除非data 正在被一些lifecycleOwner 积极观察。这只是意味着除非在Fragment 中观察到data,否则不会触发任何回调

关于mapswitchMap的进一步解释

因为我们需要做一些我们不知道什么时候完成的异步计算,所以我们不能使用mapmap 在 LiveData 之间应用线性变换。看看这个:

val liveDataOfNumbers = liveData { // Returns a LiveData<Int>
    viewModelScope.async {
         for(i in 0..10) {
             emit(i)
             delay(1000)
         }
    }
}

val liveDataOfDoubleNumbers = liveDataOfNumbers.map { number -> number * 2}

当计算是线性且简单时,这非常有用。幕后发生的事情是库正在通过MediatorLiveData 为您处理观察和发出值。这里发生的情况是,每当liveDataOfNumbers 发出一个值并且观察到liveDataOfDoubleNumbers 时,就会应用回调;所以liveDataOfDoubleNumbers 发出:0、2、4、6…

上面的sn-p等价于:

val liveDataOfNumbers = liveData { // Returns a LiveData<Int>
    viewModelScope.async {
         for(i in 0..10) {
             emit(i)
             delay(1000)
         }
    }
}

val liveDataOfDoubleNumbers = MediatorLiveData<Int>().apply {
    addSource(liveDataOfNumbers) { newNumber ->
        // Update MediatorLiveData value when liveDataOfNumbers creates a new number
        value = newNumber * 2
    }
}

但是仅仅使用map 要简单得多。

太棒了!!

现在转到您的用例。您的计算是线性的,但我们希望将这项工作推迟到后台协程。所以我们无法准确判断什么时候结束。

对于这些用例,他们创建了switchMap 运算符。它的作用与map 相同,但将所有内容包装在另一个LiveData 中。中间 LiveData 只是充当来自协程的响应的容器。

所以最终发生的是:

  1. 你的协程发布到intermediateLiveData
  2. switchMap 的作用类似于:
return MediatorLiveData().apply {
    // intermediateLiveData is what your callback generates
    addSource(intermediateLiveData) { newValue -> this.value = newValue }
} as LiveData

总结: 1.协程传值给intermediateLiveData 2.intermediateLiveData 将值传递给hourlyChartData 3. hourlyChartData 将值传递给 UI

所有内容都无需添加或删除observeForever

由于liveData {…} 是一个构建器,可以帮助我们创建异步LiveData,而无需处理实例化它们的麻烦,我们可以使用它,因此我们的switchMap 回调不那么冗长。

函数liveData 返回LiveData&lt;T&gt; 类型的实时数据。如果您的存储库调用已经返回 LiveData,那真的很简单!

val someLiveData = originalLiveData.switchMap { newValue ->
   someRepositoryCall(newValue).map { returnedRepoValue -> /*your transformation code here*/}
}

【讨论】:

  • 这条评论非常有见地且解释清楚。老实说,我很惊讶你能如此清楚地传达所有这些技巧的要点。非常感谢。明天我将在这里应用所有内容并试一试:)
  • 告诉我进展如何,晚安。希望枕头能给你带来解决方案:)
  • 再次嗨,我应用了你所说的一切,老实说,通过遵循这些实践,我的代码看起来和感觉都好得多。现在进入重点。天气的实时数据运行良好,但就图表值而言,我需要观察我的 viewModel 中的hourlyWeatherEntries,以便当其中有数据时,我可以计算图表的 LineData。我已经用代码更新了我的帖子。问题是我不知道如何将内部 observeForever 块的结果返回到异步协程 :) 我应该在这里做什么具体的事情吗?
  • 我添加了一小部分关于如何使用其他一些实用程序来执行您想要实现的目标
  • 我已经检查了该部分和文档,但我不明白为什么所有使用liveData {} 的字段都具有LiveData&lt;LiveData&lt;T&gt;&gt; 的类型?有解决方法吗?这迫使我嵌套观察者以获取我认为不理想的视图的实际值?另外,对于hourlyChartData,我还必须嵌套观察者,并且我之前提出的问题再次提出。
【解决方案2】:

分离 setupChart 和 setData 逻辑。设置图表一旦离开观察者,在观察者 setData 内部,然后调用 invalidate()。

【讨论】:

  • 我已经应用了您的更改(更新了我的帖子),但到目前为止还没有运气。现在它甚至根本不会显示图表。
  • 好的,我检查一下。
【解决方案3】:

在尝试yourlineChart.clear();yourlineChart.clearValues(); 之前,尝试注释掉invalidate() 部分以及调用搜索功能的任何位置。这将清除图表的先前值,并将形成具有新值的图表。所以,invalidate()chart.notifyDataSetChanged() 不是必需的,它应该可以解决您的问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-02-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多