Пользовательские Ленивые веселье приводит к пропуску кадра и падению - PullRequest
0 голосов
/ 12 апреля 2020

Я слежу за MVVM, используя ROOM, Retrofit, Koin DI et c. и внутри моего класса MainFragment я вызываю мою bindUI() функцию, которая отвечает за асинхронную выборку данных через viewModel с использованием сопрограмм kotlin, как вы можете видеть ниже. Теперь, когда я запускаю свое приложение, оно почти сразу падает.

Вот что я попробовал: я установил точку останова внутри bindUI() и, в частности, при первом вызове .await() на val currentWeather и запустил отладчик. Я заметил, что как только вызов await разрешен и результат возвращается в переменную, приложение вылетает, сообщая, что Skipped 1501 frames! The application may be doing too much work on its main thread., а затем Skipped 359 frames! The application may be doing too much work on its main thread.

Теперь, почему это происходит, так как я бегу эти асин c вызовы внутри потока Dispathcers.IO, и в момент создания cra sh я выполняю только один вызов await ()?

Вот мой класс MainFragment:

const val UNIT_SYSTEM_KEY = "UNIT_SYSTEM"

class MainFragment(
    private val weatherUnitConverter: WeatherUnitConverter
) : ScopedFragment() {

    // Lazy inject the view model
    private val viewModel: WeatherViewModel by viewModel()
    private lateinit var unitSystem:String
    private val TAG = MainFragment::class.java.simpleName

    // View declarations
    private lateinit var lcHourlyForecasts: LineChart
    private lateinit var weeklyForecastRCV: RecyclerView
    private lateinit var scrollView: NestedScrollView
    private lateinit var detailsExpandedArrow:ImageView
    private lateinit var detailsExpandedLayout: LinearLayout
    private lateinit var dailyWeatherDetailsHeader:LinearLayout
    private lateinit var settingsBtnImageView:ImageView
    private lateinit var unitSystemImgView:ImageView
    private lateinit var locationTxtView:TextView
    // Current weather view declarations
    private lateinit var currentWeatherDate:TextView
    private lateinit var currentWeatherTemp:TextView
    private lateinit var currentWeatherSummaryText:TextView
    private lateinit var currentWeatherSummaryIcon:ImageView
    private lateinit var currentWeatherPrecipProb:TextView
    // Today/Details weather view declarations
    private lateinit var todayHighLowTemp:TextView
    private lateinit var todayWindSpeed:TextView
    private lateinit var todayFeelsLike:TextView
    private lateinit var todayUvIndex:TextView
    private lateinit var todayPrecipProb:TextView
    private lateinit var todayCloudCover:TextView
    private lateinit var todayHumidity:TextView
    private lateinit var todayPressure:TextView
    private lateinit var todaySunriseTime:TextView
    private lateinit var todaySunsetTime:TextView

    // OnClickListener to handle the current weather's "Details" layout expansion/collapse
    private val onCurrentWeatherDetailsClicked:View.OnClickListener = 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 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
        scrollView = view.findViewById(R.id.nsv_main)
        lcHourlyForecasts = view.findViewById(R.id.lc_hourly_forecasts)
        detailsExpandedLayout = view.findViewById(R.id.ll_expandable)
        detailsExpandedArrow = view.findViewById(R.id.iv_arrow)
        dailyWeatherDetailsHeader = view.findViewById(R.id.current_weather_details_header)
        dailyWeatherDetailsHeader.setOnClickListener(onCurrentWeatherDetailsClicked)
        settingsBtnImageView = view.findViewById(R.id.settings)
        settingsBtnImageView.setOnClickListener(onSettingsButtonClicked)
        unitSystemImgView = view.findViewById(R.id.unitSystemImg)
        locationTxtView = view.findViewById(R.id.location)
        initCurrentWeatherViews(view)
        initTodayWeatherViews(view)
        // RCV initialization
        weeklyForecastRCV = view.findViewById(R.id.weekly_forecast_rcv)
        weeklyForecastRCV.adapter = WeeklyWeatherAdapter(listOf(),viewModel.preferences, this,weatherUnitConverter) // init the adapter with empty data
        weeklyForecastRCV.setHasFixedSize(true)
        // Disable nested scrolling to control the RCV scrolling via the parent NestedScrollView
        weeklyForecastRCV.isNestedScrollingEnabled = false

        return view
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        initLineChart()
        bindUI()
    }


    private fun SharedPreferences.stringLiveData(key: String, defValue: String): SharedPreferenceLiveData<String> {
        return SharedPreferenceStringLiveData(this, key, defValue)
    }

    private fun bindUI() = launch(Dispatchers.Main) {
        //TODO:sp get the coordinates dynamically
        viewModel.setLocCoordinates(37.8267,-122.4233)
        // fetch current weather
        val currentWeather = viewModel.currentWeatherData.await()
        // fetch weekly weather
        val weeklyWeather = viewModel.weeklyWeatherEntries.await()
        // fetch the location
        val weatherLocation = viewModel.weatherLocation.await()

        // Observe the location for changes
        weatherLocation.observe(viewLifecycleOwner, Observer { location ->
            if(location == null) return@Observer
            launch {
                updateLocation(location)
            }
        })

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

            // Observe the unit system sharedPrefs live data for changes
            viewModel.preferences.stringLiveData(UNIT_SYSTEM_KEY, UnitSystem.SI.name.toLowerCase(Locale.ROOT))
                .observe(viewLifecycleOwner, Observer {unitSystem ->
                    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)
        })

        // observe the weekly weather live data
        weeklyWeather.observe(viewLifecycleOwner, Observer {weatherEntries ->
            if(weatherEntries == null) return@Observer
            // update the recyclerView with the new data
            (weeklyForecastRCV.adapter as WeeklyWeatherAdapter).updateWeeklyWeatherData(weatherEntries)
            initTodayData(weatherEntries[0])
        })
    }

    /**
     * Uses the location param's lat & longt values
     * to determine the selected location and updates
     * the view.
     */
    private suspend fun updateLocation(location: WeatherLocation) {
        withContext(Dispatchers.IO) {
            val geocoder = Geocoder(activity,Locale.getDefault())
            try {
                val addr = geocoder.getFromLocation(location.latitude,location.longitude,1)
                val adobj = addr[0]
                locationTxtView.text =  adobj.countryName
            } catch (e:IOException) {
                Log.d(TAG, e.printStackTrace().toString())
            }
        }
    }

    /**
     * Initializes the views for the current weather.
     */
    private fun initCurrentWeatherViews(view: View) {
        currentWeatherDate = view.findViewById(R.id.current_weather_date)
        currentWeatherTemp = view.findViewById(R.id.current_temp_main)
        currentWeatherSummaryText = view.findViewById(R.id.current_weather_summary_text)
        currentWeatherSummaryIcon = view.findViewById(R.id.current_weather_summary_icon)
        currentWeatherPrecipProb = view.findViewById(R.id.current_weather_precip_text)
    }

    /**
     * Initializes the views for the Detailed Today weather view.
     */
    private fun initTodayWeatherViews(view: View?) {
        if(view == null) return
        todayHighLowTemp = view.findViewById(R.id.today_lowHighTemp)
        todayWindSpeed = view.findViewById(R.id.today_windSpeed)
        todayFeelsLike = view.findViewById(R.id.today_feelsLike)
        todayUvIndex = view.findViewById(R.id.today_uvIndex)
        todayPrecipProb = view.findViewById(R.id.today_precipProb)
        todayCloudCover = view.findViewById(R.id.today_cloudCover)
        todayHumidity = view.findViewById(R.id.today_humidity)
        todayPressure = view.findViewById(R.id.today_pressure)
        todaySunriseTime = view.findViewById(R.id.today_sunriseTime)
        todaySunsetTime = view.findViewById(R.id.today_sunsetTime)
    }

    private fun setUnitSystemImgView(unitSystem:String) {
        val resource = when(unitSystem) {
            UnitSystem.SI.name.toLowerCase(Locale.ROOT)
                -> R.drawable.ic_celsius
            UnitSystem.US.name.toLowerCase(Locale.ROOT)
                -> R.drawable.ic_fahrenheit
            else -> R.drawable.ic_celsius
        }
        unitSystemImgView.setImageResource(resource)
    }

    /**
     * Links the data to the view for the Today(Details) Weather View.
     */
    private fun initTodayData(weekDayWeatherEntry: WeekDayWeatherEntry) {
        // Observe the unit system sharedPrefs live data for changes
        viewModel.preferences.stringLiveData(UNIT_SYSTEM_KEY, UnitSystem.SI.name.toLowerCase(Locale.ROOT))
            .observe(viewLifecycleOwner, Observer {unitSystem ->
                when(unitSystem) {
                    UnitSystem.SI.name.toLowerCase(Locale.ROOT) -> {
                        setTodayWeatherLowHighTemp(weekDayWeatherEntry.temperatureLow,weekDayWeatherEntry.temperatureHigh)
                        setTodayWeatherWindSpeed(weekDayWeatherEntry.windSpeed,unitSystem)
                        setTodayWeatherFeelsLike(weekDayWeatherEntry.apparentTemperatureLow,weekDayWeatherEntry.apparentTemperatureHigh)
                    }
                    UnitSystem.US.name.toLowerCase(Locale.ROOT) -> {
                        setTodayWeatherLowHighTemp(weatherUnitConverter.convertToFahrenheit(
                            weekDayWeatherEntry.temperatureLow),
                            weatherUnitConverter.convertToFahrenheit(
                            weekDayWeatherEntry.temperatureHigh))
                        setTodayWeatherWindSpeed(weatherUnitConverter.convertToMiles(weekDayWeatherEntry.windSpeed),unitSystem)
                        setTodayWeatherFeelsLike(weatherUnitConverter.convertToFahrenheit(
                            weekDayWeatherEntry.apparentTemperatureLow)
                            ,weatherUnitConverter.convertToFahrenheit(weekDayWeatherEntry.apparentTemperatureHigh))
                    }
                }
            })
        setTodayWeatherUvIndex(weekDayWeatherEntry.uvIndex)
        setTodayWeatherPrecipProb(weekDayWeatherEntry.precipProbability)
        setTodayWeatherCloudCover(weekDayWeatherEntry.cloudCover)
        setTodayWeatherHumidity(weekDayWeatherEntry.humidity)
        setTodayWeatherPressure(weekDayWeatherEntry.pressure)
        setTodayWeatherSunriseTime(weekDayWeatherEntry.sunriseTime)
        setTodayWeatherSunsetTime(weekDayWeatherEntry.sunsetTime)
    }
...
}

WeatherViewModel.kt:

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

    private var mLatitude:Double = 0.0
    private var mLongitute:Double = 0.0
    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 by lazyDeferred {
        forecastRepository.getCurrentWeather(mLatitude, mLongitute)
    }

    val weeklyWeatherEntries by lazyDeferred {
        val currentDateEpoch = LocalDate.now().toEpochDay()
        forecastRepository.getWeekDayWeatherList(mLatitude, mLongitute, currentDateEpoch)
    }

    val weatherLocation by lazyDeferred {
        forecastRepository.getWeatherLocation()
    }

    fun setLocCoordinates(latitude:Double,longitude:Double) {
        mLatitude = latitude
        mLongitute = longitude
    }
}

Вот мой пользовательский Lazy<Deferred<T>> прикол внутри моего файла Delegates.kt:

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

Вот мой класс репозитория на всякий случай:

private const val WEEKLY_FORECAST_DAYS_COUNT = 7

/**
 * The Repository class responsible
 * for caching the downloaded weather data
 * and for swapping between different data sources.
 */
class ForecastRepositoryImpl(
    private val currentWeatherDao: CurrentWeatherDao,
    private val weekDayWeatherDao: WeekDayWeatherDao,
    private val weatherLocationDao: WeatherLocationDao,
    private val locationProvider: LocationProvider,
    private val weatherNetworkDataSource: WeatherNetworkDataSource
) : ForecastRepository {

    init {
        weatherNetworkDataSource.apply {
            // Persist downloaded data
            downloadedCurrentWeatherData.observeForever { newCurrentWeather: CurrentWeatherResponse? ->
                persistFetchedCurrentWeather(newCurrentWeather!!)
            }
            downloadedWeeklyWeatherData.observeForever { newWeeklyWeather: WeeklyWeatherResponse? ->
                persistFetchedWeeklyWeather(newWeeklyWeather!!)
            }
        }
    }

    override suspend fun getCurrentWeather(latitude:Double,longitude:Double): LiveData<CurrentWeatherEntry> {
        return withContext(Dispatchers.IO) {
            initWeatherData(latitude,longitude)
            return@withContext currentWeatherDao.getCurrentWeather()
        }
    }

    override suspend fun getWeekDayWeatherList(latitude: Double,longitude: Double,time:Long): LiveData<out List<WeekDayWeatherEntry>> {
        return withContext(Dispatchers.IO) {
            initWeatherData(latitude,longitude)
            return@withContext weekDayWeatherDao.getFutureWeather(time)
        }
    }

    override suspend fun getWeatherLocation(): LiveData<WeatherLocation> {
        return withContext(Dispatchers.IO) {
            return@withContext weatherLocationDao.getWeatherLocation()
        }
    }

    private suspend fun initWeatherData(latitude:Double,longitude:Double) {
        // retrieve the last weather location from room
        val lastWeatherLocation = weatherLocationDao.getWeatherLocation().value

        if(lastWeatherLocation == null ||
                locationProvider.hasLocationChanged(lastWeatherLocation)) { // then this is the first time we are launching the app
            fetchCurrentWeather()
            fetchWeeklyWeather()
            return
        }

        if(isFetchCurrentNeeded(lastWeatherLocation.zonedDateTime))
            fetchCurrentWeather()

        if(isFetchWeeklyNeeded())
            fetchWeeklyWeather()
    }

    /**
     * Checks if the current weather data should be re-fetched.
     * @param lastFetchedTime The time at which the current weather data were last fetched
     * @return True or false respectively
     */
    private fun isFetchCurrentNeeded(lastFetchedTime: ZonedDateTime) : Boolean {
        val thirtyMinutesAgo = ZonedDateTime.now().minusMinutes(30)
        return lastFetchedTime.isBefore(thirtyMinutesAgo)
    }

    /**
     * Fetches the Current Weather data from the WeatherNetworkDataSource.
     */
    private suspend fun fetchCurrentWeather() {
        weatherNetworkDataSource.fetchCurrentWeather(
            locationProvider.getPreferredLocationLat(),
            locationProvider.getPreferredLocationLong()
        )
    }

    private fun isFetchWeeklyNeeded(): Boolean {
        val todayEpochTime = LocalDate.now().toEpochDay()
        val futureWeekDayCount = weekDayWeatherDao.countFutureWeekDays(todayEpochTime)
        return futureWeekDayCount < WEEKLY_FORECAST_DAYS_COUNT
    }

    private suspend fun fetchWeeklyWeather() {
        weatherNetworkDataSource.fetchWeeklyWeather(
            locationProvider.getPreferredLocationLat(),
            locationProvider.getPreferredLocationLong()
        )
    }

    /**
     * Caches the downloaded current weather data to the local
     * database.
     * @param fetchedCurrentWeather The most recently fetched current weather data
     */
    private fun persistFetchedCurrentWeather(fetchedCurrentWeather:CurrentWeatherResponse) {
        // Using a GlobalScope since a Repository class doesn't have a lifecycle
        GlobalScope.launch(Dispatchers.IO) {
            // cache the data
            currentWeatherDao.upsert(fetchedCurrentWeather.currentWeatherEntry)
            weatherLocationDao.upsert(fetchedCurrentWeather.location)
        }
    }

    /**
     * Caches the downloaded weekly weather data to the local
     * database.
     * @param fetchedWeeklyWeather  The most recently fetched weekly weather data
     */
    private fun persistFetchedWeeklyWeather(fetchedWeeklyWeather: WeeklyWeatherResponse) {

        fun deleteOldData() {
            val time = LocalDate.now().toEpochDay()
            weekDayWeatherDao.deleteOldEntries(time)
        }

        GlobalScope.launch(Dispatchers.IO) {
            deleteOldData()
            val weekDayEntriesList = fetchedWeeklyWeather.weeklyWeatherContainer.weekDayEntries
            weekDayWeatherDao.insert(weekDayEntriesList)
        }
    }
}

РЕДАКТИРОВАТЬ: Вот журнал cra sh, о котором я узнал ранее сегодня:

2020-04-13 01:43:48.628 26875-26904/com.nesoinode.flogaweather E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-2
    Process: com.nesoinode.flogaweather, PID: 26875
    java.lang.NullPointerException: Attempt to invoke virtual method 'int com.nesoinode.flogaweather.model.db.entity.WeatherLocation.getId()' on a null object reference
        at com.nesoinode.flogaweather.model.db.dao.WeatherLocationDao_Impl$1.bind(WeatherLocationDao_Impl.java:34)
        at com.nesoinode.flogaweather.model.db.dao.WeatherLocationDao_Impl$1.bind(WeatherLocationDao_Impl.java:26)
        at androidx.room.EntityInsertionAdapter.insert(EntityInsertionAdapter.java:63)
        at com.nesoinode.flogaweather.model.db.dao.WeatherLocationDao_Impl.upsert(WeatherLocationDao_Impl.java:52)
        at com.nesoinode.flogaweather.model.repository.ForecastRepositoryImpl$persistFetchedCurrentWeather$1.invokeSuspend(ForecastRepositoryImpl.kt:131)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:561)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:727)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:667)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:655)

Он указывает на эту часть моего класса репозитория как причину root. Хотя я не могу сказать, почему.

private fun persistFetchedCurrentWeather(fetchedCurrentWeather:CurrentWeatherResponse) {
    // Using a GlobalScope since a Repository class doesn't have a lifecycle
    GlobalScope.launch(Dispatchers.IO) {
        // cache the data
        currentWeatherDao.upsert(fetchedCurrentWeather.currentWeatherEntry)
        weatherLocationDao.upsert(fetchedCurrentWeather.location)
    }
}

ОБНОВЛЕНИЕ № 2:

CurrentWeatherEntry:

const val CURRENT_WEATHER_ID = 0

@Entity(tableName = "current_weather")
data class CurrentWeatherEntry(
    val time: Long, // epoch timestamp
    val icon: String,
    val summary: String,
    val precipProbability: Double,
    val temperature: Double
) {
    @PrimaryKey(autoGenerate = false)
    var id:Int = CURRENT_WEATHER_ID
}

WeatherLocation:

const val WEATHER_LOCATION_ID = 0

@Entity(tableName = "weather_location")
data class WeatherLocation(
    val latitude: Double,
    val longitude: Double,
    val timezone: String
) {
    @PrimaryKey(autoGenerate = false)
    var id:Int = WEATHER_LOCATION_ID

    private var epochTimeVal:Long = 0

    val zonedDateTime:ZonedDateTime
        get() {
            val instant = Instant.ofEpochMilli(this.epochTimeVal)
            val zoneId = ZoneId.of(timezone)
            return ZonedDateTime.ofInstant(instant,zoneId)
        }

    fun setEpochTimeVal(time:Long) {
        this.epochTimeVal = time}
    fun getEpochTimeVal() : Long = epochTimeVal
}

и CurrentWeatherResponse:

data class CurrentWeatherResponse(
    // Tells GSON that the "currently" field of the JSON returned by the
    // API should be tied with our CurrentWeatherEntry data class
    @SerializedName("currently")
    val currentWeatherEntry: CurrentWeatherEntry,
    @Embedded
    val location: WeatherLocation
) {
    init {
        location.setEpochTimeVal(currentWeatherEntry.time)
    }
}

Ответы [ 2 ]

1 голос
/ 13 апреля 2020

В соответствии с вашим дальнейшим диагнозом, проблема не связана с lazyDeferred, блокирующим основной поток или сопрограммы в целом. Ваш CurrentWeatherDao иногда возвращает CurrentWeatherResponse с location == null.

1 голос
/ 12 апреля 2020

Вы никогда не указывали, где forecastRepository.getWeatherLocation() должно выполняться, чтобы оно выполнялось диспетчером вашей bindUI функции, которая Dispatchers.Main. Это означает, что запрос блокирует ваш поток пользовательского интерфейса и вызывает предупреждение, которое вы видите в журнале.

Необходимо указать, что он выполняется на отдельном диспетчере, чтобы пользовательский интерфейс мог продолжать обновление в обычном режиме:

lazyDeferred {
    withContext(Dispatchers.IO) {
        forecastRepository.getWeatherLocation()
    }
}

Как отдельная проблема, ваш lazyDeferred немного избыточен в том смысле, что он "двойной" -лазный. Вы можете удалить внешний Lazy<T>, и он все равно будет работать точно так же, или удалить start = CoroutineStart.LAZY, и результат будет получен немного раньше. (Это существенно зависит от того, начинается ли запрос при разрешении Lazy или при вызове Deferred.await)

...