Я слежу за 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)
}
}