在我的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 侧等待,看起来像是一种代码气味…
这将导致引发异常或取消异常。
只需观察
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.map
和LiveData.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
,否则不会触发任何回调有关
map
和switchMap
的进一步说明由于我们需要执行一些不知道何时要进行的异步计算,因此无法使用
map
。 map
在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
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<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/