android - 使用新数据调用invalidate()后,MP Android Chart消失

标签 android kotlin mpandroidchart kotlin-coroutines

在我的Weather应用程序中,我有一个MainFragment,其中有一个按钮可以打开一个不同的片段(SearchFragment)(通过替换),允许用户选择一个位置,然后获取该位置的天气数据并将其加载到各种 View 中,包括MPAndroid线图。我的问题是,每当我从搜索片段中回来时,尽管都会为图表获取新数据,并且在invalidate()之后,我会调用chart.notifyDataSetChanged()chart.invalidate()(也尝试使用chart.postInvalidate(),因为在另一个线程上工作时建议使用该代码)。所谓的图表根本就消失了。我在这里想念什么?

主片段:

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()
}
}

最佳答案

如果没有整个环境,我真的很难帮助您调试整个过程,但是很高兴为您提供一些乍一看似乎有些错的事情。

首先,我会避免自己管理所有的CoroutinesScope和生命周期,而且很容易出错。因此,我将依靠Android团队已经完成的工作。快速浏览here,它非常容易设置和使用。开发者经验很棒。

Deferred上发布LiveData并在 View 侧等待,看起来像是一种代码气味…

  • 如果出现网络错误怎么办?
    这将导致引发异常或取消异常。
  • 如果任务已经执行并且导致某种类型的UI一致性问题,该怎么办?这些是我不太想处理的几个极端情况。

  • 只需观察LiveData即可,因为它的主要目的是:它是一个值持有者,用于在Fragment中经历多个生命周期事件。因此,一旦重新创建 View ,就可以在ViewModel中的LiveData中准备好该值。

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

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

    以下是有关如何在ViewModel中使用这些实用程序以确保事物或协程的生命周期导致任何问题的代码段:

    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绑定(bind)在一起,而无需编写处理CoroutineScope寿命的单行代码。当ViewModel死后,范围也将消失。当重新创建 View 时,例程将不会执行两次,并且可以读取值。

    关于图表的配置:我强烈建议您在创建 View 后立即配置图表,因为它是紧密相连的。您只需一次就可以进行配置,如果多次执行某些指令(我相信这可能会发生在您的身上),可能会导致视觉错误,这是因为我在使用Piechart的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

    它的作用是,每次源LiveData发出一个值时,您都将得到一个回调,并且期望您返回一个具有新值的新实时数据。

    您可以安排以下内容:

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

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

    有关mapswitchMap的进一步说明

    由于我们需要执行一些不知道何时要进行的异步计算,因此无法使用mapmap在LiveDatas之间应用线性变换。看一下这个:

    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…

    上面的代码段等效于以下内容:

    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只是充当协程程序响应的容器。

    所以最终发生的是:
  • 您的协程发布到intermediateLiveData
  • switchMap类似于:

  • return MediatorLiveData().apply {
        // intermediateLiveData is what your callback generates
        addSource(intermediateLiveData) { newValue -> this.value = newValue }
    } as LiveData
    

    加起来:
    1.协程将值传递给intermediateLiveData2. intermediateLiveData将值传递给hourlyChartData3. hourlyChartData将值传递给UI

    一切都无需添加或删除observeForever
    由于liveData {…}是可帮助我们创建异步LiveData的构建器,而无需处理实例化它们的麻烦,因此我们可以使用它,因此switchMap回调的详细程度较低。

    函数liveData返回LiveData<T>类型的实时数据。如果您的存储库调用已经返回LiveData,那就非常简单!

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

    关于android - 使用新数据调用invalidate()后,MP Android Chart消失,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62044353/

    相关文章:

    Android MVVM 多 API 调用

    Android 在RelativeLayout中填充左动画

    android - 如何在 MPAndroidChart 中制作动画?

    android - React-Native 通过 HTTP 发送 multipart/form-data

    android - 在屏幕键盘可见的情况下,在 Android 上离开带有 map 的页面时出错

    java - 不幸的是MyApp已停止。我该如何解决?

    Kotlin 等价于 Optional.map 和方法引用

    Android 改造和 GSON : JSON response returns object or string as property value

    java - 如何删除或编辑 MPAndroidChart - StackedBarChart - 条目?

    java - 如何设置 mpandroidchart 条形图中条形(y 轴)的比例