Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to refresh ViewModel data using Kotlin lazy loading coroutine?

I am trying to implement a SwipeToRefreshLayout in a weather app I'm building. When the user swipes to refresh, the data in the ViewModel should be updated, and then the view should be updated accordingly.

Here is a snippet of my CurrentWeatherFragment:

override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        viewModel = ViewModelProviders.of(this, viewModelFactory)
                .get(WeatherResponseViewModel::class.java)

        pullToRefresh.setOnRefreshListener(this)

        bindUI()
    }

    override fun onRefresh() {
        viewModel = ViewModelProviders.of(this, viewModelFactory)
                .get(WeatherResponseViewModel::class.java)

        bindUI()
        pullToRefresh.isRefreshing = false
    }

    private fun bindUI() = launch {
        val currentWeather = viewModel.weather.await()
        currentWeather.observe(this@CurrentWeatherFragment, Observer {
            if (it == null) return@Observer

            loading_icon.visibility = View.GONE

            updateLocation("Raleigh")
            updateDateToToday()
            updateTemperatures(it.currently.temperature.roundToInt(),
                    it.currently.apparentTemperature.roundToInt(),
                    it.daily.data[0].temperatureMin.roundToInt(),
                    it.daily.data[0].temperatureMax.roundToInt())
            updateDescription(it.currently.summary)
            updateEnvironmentals((it.currently.humidity * 100).roundToInt(), it.currently.windSpeed.roundToInt())
            updateWeatherIcon(it.currently.icon)
            updateTempChart(it)
            updatePrecipChart(it)
        })
    }

my ViewModel:

class WeatherResponseViewModel (
        private val forecastRepository: ForecastRepository,
        unitProvider: UnitProvider
) : ViewModel() {

    private val unitSystem = unitProvider.getUnitSystem()

    val isMetric: Boolean
        get() = unitSystem == UnitSystem.METRIC

    val weather by lazyDeferred {
        forecastRepository.getWeather()
    }

}

and my lazyDeferred implementation:

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

Currently, with this setup, the forecastRepository.getWeather() function in the ViewModel is called when the fragment is loaded either on app launch or when switching to it, but on the swipe to refresh, it isn't being called. How can I get the weather variable in the ViewModel to update so that the view can observe the change?

like image 627
Cameron Avatar asked Aug 31 '25 03:08

Cameron


2 Answers

First, There are some wrong implementations about Coroutine and ViewModel in your code.

  1. Do not re-instantiate ViewModel again in onRefresh.

    Instead, just make and call refresh method in your viewmodel when SwipeRefreshLayout is pulled.

  2. Do not use GlobalScope in your Application if you can.

    It can cause Work Leak and doesn't follow Structured concurrency rule.

    Instead, use coroutineScope{} block.

  3. Do not use LifecycleOwner of Fragment for observing LiveData in fragment. It can generate duplicated observer for LiveData.

    Instead, use viewLifecycleOwner instance of fragment.

    This is becuase when the fragment is restored in back stack, the views of fragment is re-created, but fragment.

    If you match your observer lifecycle with views in fragment, Ovservers won't remain after view is destoryed.


Way to Refresh

Don't use lazy block. It seems useless like @Stanislav Bondar's answer.

like image 172
MJ Studio Avatar answered Sep 02 '25 18:09

MJ Studio


From Kotlin Delegated Properties:

lazy properties: the value gets computed only upon first access;

Don't use lazy for mutable data

like image 37
Stanislav Bondar Avatar answered Sep 02 '25 16:09

Stanislav Bondar