Это мой первый раз, когда я работаю с MVVM-аркой, и я следую руководству по созданию приложения для погоды. Теперь я наблюдаю некоторые LiveData
в методе onBindUIAsync()
моего фрагмента и использую launch{}
, чтобы запустить сопрограмму для него внутри onActivityCreated()
согласно документам.
У меня есть две настройки местоположения в моих настройках PreferenceFragment
, одна для использования текущего местоположения устройства и другая для использования пользовательской в текстовой форме (например, «Нью-Йорк»). Каждый раз, когда пользователь меняет один из этих двух параметров, я хочу повторно получать данные о погоде, когда пользователь возвращается к главному фрагменту. Однако я заметил, что хотя это действительно происходит, загрузка данных в представление занимает значительно больше времени по сравнению с производительностью учебника. Я где-то слишком много работаю? Я повторяю операции?
Я также использовал оператор Log.d()
в методе persistFetchedCurrentWeather()
моего репозитория, чтобы проверить, как часто он вызывается, и я увидел, что каждый раз, когда MainFragment помещается в контейнер MainActivity, он получает звонил дважды. Означает ли это, что я получаю данные дважды, отсюда и задержка загрузки их в представление?
PS Я не знаю, как правильно озаглавить эту проблему, поэтому, пожалуйста, исправьте меня.
MainActivity.kt:
class MainActivity : AppCompatActivity() {
private lateinit var toolbar: Toolbar
private lateinit var backBtn:ImageView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
// Koin Fragment Factory
setupKoinFragmentFactory()
toolbar = findViewById(R.id.toolbar)
backBtn = findViewById(R.id.backBtn)
backBtn.setOnClickListener { onBackPressed() }
// Make the activity extend behind the status bar & make the status bar transparent
window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.container,
MainFragment::class.java,null,null
)
.commitNow()
}
requestLocationPermission()
if(!hasLocationPermission()) requestLocationPermission()
}
private fun hasLocationPermission(): Boolean {
return ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
}
private fun requestLocationPermission() {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
,PERMISSION_ACCESS_FINE_LOCATION_REQUEST_CODE)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
if(requestCode == PERMISSION_ACCESS_FINE_LOCATION_REQUEST_CODE) {
if(grantResults.isEmpty() || grantResults[0] == PackageManager.PERMISSION_DENIED)
Toast.makeText(this,"Please enable location permissions in settings",Toast.LENGTH_LONG).show()
}
}
fun openSettingsPage() {
toolbar.visibility = View.VISIBLE
supportFragmentManager.beginTransaction()
.replace(R.id.container,
SettingsFragment::class.java,null,null
)
.commitNow()
}
override fun onBackPressed() {
toolbar.visibility = View.GONE
supportFragmentManager.beginTransaction()
.replace(R.id.container,
MainFragment::class.java,null,null
)
.commitNow()
}
}
MainFragment:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setUpChart()
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
}
}
private fun bindUIAsync() = async(Dispatchers.Main) {
// 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 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])
})
// Observe the location for changes
weatherLocation.observe(viewLifecycleOwner, Observer { location ->
if (location == null) return@Observer
locationTxtView.text = location.getLocationName(this@MainFragment.requireContext())
})
return@async true
}
Класс репозитория:
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(): LiveData<CurrentWeatherEntry> {
return withContext(Dispatchers.IO) {
initWeatherData()
return@withContext currentWeatherDao.getCurrentWeather()
}
}
override suspend fun getWeekDayWeatherList(time:Long): LiveData<out List<WeekDayWeatherEntry>> {
return withContext(Dispatchers.IO) {
initWeatherData()
return@withContext weekDayWeatherDao.getFutureWeather(time)
}
}
override suspend fun getWeatherLocation(): LiveData<WeatherLocation> {
return withContext(Dispatchers.IO) {
return@withContext weatherLocationDao.getWeatherLocation()
}
}
private suspend fun initWeatherData() {
// retrieve the last weather location from room
val lastWeatherLocation = weatherLocationDao.getWeatherLocation().value
if(lastWeatherLocation == null ||
locationProvider.hasLocationChanged(lastWeatherLocation)) {
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
Log.d("REPO","caching 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)
}
}
}
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 by lazyDeferred {
forecastRepository.getCurrentWeather()
}
val weeklyWeatherEntries by lazyDeferred {
val currentDateEpoch = LocalDate.now().toEpochDay()
forecastRepository.getWeekDayWeatherList(currentDateEpoch)
}
val weatherLocation by lazyDeferred {
withContext(Dispatchers.IO) {
forecastRepository.getWeatherLocation()
}
}
}
LazyDeferred определение:
fun<T> lazyDeferred(block: suspend CoroutineScope.() -> T) : Lazy<Deferred<T>> {
return lazy {
GlobalScope.async {
block.invoke(this)
}
}
}