Наблюдение LiveData с использованием сопрограмм занимает слишком много времени, чтобы обновить представление - PullRequest
0 голосов
/ 21 апреля 2020

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