MP Android Диаграмма исчезает после вызова invalidate () с новыми данными - PullRequest
4 голосов
/ 27 мая 2020
• 1000 включая MP Android LineChart. Моя проблема в том, что всякий раз, когда я возвращаюсь из фрагмента поиска, хотя для диаграммы выбираются новые данные, и я вызываю chart.notifyDataSetChanged() & chart.invalidate() (также пробовал chart.postInvalidate(), поскольку это было предложено при работе с другим потоком) после вызова invalidate () график просто исчезает. Что мне здесь не хватает?

MainFragment:

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

}

LazyDeferred:

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

Ответы [ 3 ]

1 голос
/ 15 июня 2020

Без всей среды мне действительно сложно помочь вам отладить все это, но я рад предоставить вам пару вещей, которые на первый взгляд кажутся немного странными.

Прежде всего all Я бы избегал управления всеми CoroutinesScopes и жизненными циклами самостоятельно, и это легко сделать неправильно. Так что я бы положился на то, что команда Android уже сделала. Взгляните на здесь , это действительно легко установить и использовать. Отличный опыт разработки.

Размещение Deferred на LiveData и ожидание на стороне просмотра похоже на запах кода ...

  • Что, если есть сеть ошибка? Это приведет к возникновению исключения или исключения отмены.

  • Что, если задача уже была выполнена и вызывает некоторую проблему согласованности пользовательского интерфейса? Это пара крайних случаев, с которыми я бы не хотел справляться.

Просто обратите внимание на LiveData, поскольку это его основная цель: это держатель значения, и он предназначен для того, чтобы пережить несколько событий жизненного цикла в Fragment. Итак, как только представление воссоздано, значение готово в LiveData внутри ViewModel.

Ваша функция lazyDeferred довольно умна, но в мире Android она также опасна. Эти сопрограммы не живут внутри какой-либо области действия, управляемой жизненным циклом, поэтому у них действительно высока вероятность утечки информации. И поверьте мне, вы не хотите утечки каких-либо сопрограмм, поскольку они продолжают свою работу даже после разрушения модели представления и фрагмента, чего вы определенно не хотите.

Все это легко исправить с помощью зависимости Я уже упоминал, что я вставлю сюда еще раз

Вот отрывок о том, как вы можете использовать эти утилиты в своей ViewModel, чтобы убедиться, что жизненный цикл вещей и сопрограммы не вызывают issues:

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 умирает, область видимости умирает. при повторном создании представления подпрограммы не будут выполняться дважды, и значения будут готовы к чтению.

Что касается конфигурации диаграммы: я настоятельно рекомендую вам настроить диаграмму, как только вы создадите вид, поскольку они сильно связаны между собой. Конфигурация - это то, что вы хотите сделать только один раз и может вызвать визуальные ошибки, если некоторые инструкции выполняются более одного раза (что, я считаю, может происходить с вами), просто так, потому что у меня были проблемы с MP Android с использованием круговой диаграммы.

Подробнее на диаграмме: все логики c построения LineData лучше бы размещать в фоновом потоке и отображать через LiveData на стороне ViewModel, как если бы вы делали все другое

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

Pro Kotlin совет: Избегайте повторений во время этих длинных функций конфигурации MP Android.

Вместо:

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 намного проще .

Fantasti c !!

Теперь перейдем к вашему варианту использования. Вычисления линейны, но мы хотим отложить эту работу до фоновой сопрограммы. Поэтому мы не можем точно сказать, когда что-то закончится.

Для этих случаев использования они создали оператор 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 передает значение пользовательскому интерфейсу

И все, не добавляя и не удаляя observeForever

Поскольку liveData {…} - это конструктор, который помогает нам создавать асинхронные LiveDatas без необходимости создавать их экземпляры, мы можем использовать его, чтобы наш обратный вызов switchMap был менее подробным.

Функция liveData возвращает живые данные типа LiveData<T>. Если ваш вызов репозитория уже возвращает LiveData, это действительно просто!

val someLiveData = originalLiveData.switchMap { newValue ->
   someRepositoryCall(newValue).map { returnedRepoValue -> /*your transformation code here*/}
}
0 голосов
/ 11 июня 2020

Попробуйте закомментировать часть invalidate() и где бы вы ни вызывали свою функцию поиска, прежде чем она попробует yourlineChart.clear(); или yourlineChart.clearValues();. Это очистит предыдущие значения диаграммы и сформирует диаграмму с новыми значениями. Таким образом, invalidate() и chart.notifyDataSetChanged() не нужны, и это должно решить вашу проблему.

0 голосов
/ 31 мая 2020

Разделите логику setupChart и setData. Настройте диаграмму один раз вне наблюдателя, внутри наблюдателя setData и после этого вызовите invalidate ().

...