【问题标题】:Wait with launching coroutine as long as first one is not finished. Synchronization mechanism只要第一个未完成,就等待启动协程。同步机制
【发布时间】:2021-10-10 18:05:24
【问题描述】:

我正在学习 android 开发,我决定使用来自名为 open water map 的服务的 api 构建一个天气应用程序。不幸的是,我遇到了以下问题:

为了获取通缉城市的天气数据,我首先需要执行获取地理坐标的请求。所以我需要做的是创建一个请求,等到它完成,然后使用从第一个接收到的数据执行另一个请求。

这是我的位置视图模型的样子:

class LocationViewModel constructor(private val repository: WeatherRepository): ViewModel() {

    val location = MutableLiveData<List<GeocodingModel>>()

    private val API_KEY = „xxxxxxxxxxxxxxxxxxxxxxxxx”


    fun refresh() {
        CoroutineScope(Dispatchers.IO).launch {
        // call fetch location here in coroutine
        }
    }

    private suspend fun fetchLocation(): Response<GeocodingModel> {
        return repository.getCoordinates(
                "Szczecin",
                API_KEY
            )
    }
}

这就是我的天气视图模型的样子”

class WeatherSharedViewModel constructor(private val repository: WeatherRepository): ViewModel() {

    private val API_KEY = „xxxxxxxxxxxxxxxxxxxxxxxxx”

    val weather = MutableLiveData<List<SharedWeatherModel>>()

    val weatherLoadError = MutableLiveData<Boolean>()

    val loading = MutableLiveData<Boolean>()


    fun refresh(lat: String, lon: String) {
        loading.value = true

        CoroutineScope(Dispatchers.IO).launch {
        // call fetchWeather here in coroutine
        }

        loading.value = false
    }

    private suspend fun fetchWeather(lat: String, lon: String): Response<SharedWeatherModel> {
                return repository.getWeather(
                    lat,
                    lon,
                    "minutely,hourly,alerts",
                    "metric",
                    API_KEY
                )
    }
}

我以这种方式在片段中使用两个视图模型:

override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        val weatherService = WeatherApi.getInstance()
        val repository = WeatherRepository(weatherService)
        locationViewModel = ViewModelProvider(requireActivity(), ViewModelFactory(repository)).get(LocationViewModel::class.java)
        weatherViewModel = ViewModelProvider(requireActivity(), ViewModelFactory(repository)).get(WeatherSharedViewModel::class.java)

        locationViewModel.refresh()
        Log.d(TAG, "lat: ${locationViewModel.location.value?.get(0)?.get(0)?.lat.toString()}, lon: ${locationViewModel.location.value?.get(0)?.get(0)?.lon.toString()}")
        weatherViewModel.refresh(
            locationViewModel.location.value?.get(0)?.get(0)?.lat.toString(),
            locationViewModel.location.value?.get(0)?.get(0)?.lon.toString()
        )

        val weatherList = view?.findViewById<RecyclerView>(R.id.currentWeatherList)
        weatherList?.apply {
            layoutManager = LinearLayoutManager(context)
            adapter = currentWeatherAdapter
        }

        val cityList = view?.findViewById<RecyclerView>(R.id.currentCityList)
        cityList?.apply {
            layoutManager = LinearLayoutManager(context)
            adapter = currentLocationAdapter
        }

        observerLocationViewModel()
        observeWeatherViewModel()
    }

因此,在启动时,两个模型都会刷新,这意味着请求已发出。我试图以某种方式同步这些调用,但我最后一次尝试结束了传递给天气视图模型的刷新方法的数据为空。所以问题是两个协程一个接一个地启动,第一个没有等待第二个。

主要问题:协程中有同步机制吗?只要第一个未完成,我可以启动一个协程并等待启动第二个协程?

【问题讨论】:

    标签: android kotlin mvvm retrofit2 kotlin-coroutines


    【解决方案1】:

    您违反了学习如何编写 CLEAN 代码所需的“单一职责原则”。这就是您遇到此类问题的原因。 stackoverflow的一位成员已经深入解释过:single responsibility

    【讨论】:

      【解决方案2】:

      一些提示:

      • 您的总体设计有些复杂,因为您尝试使用协程更新 LiveData,但一个 LiveData 的公开数据是由另一个 LiveData 确定的。如果您需要能够访问该城市,即使您已经拥有该城市的天气,这在理论上是可以的,但是由于您已经在两个 ViewModel 之间分割了这种行为,您最终不得不在外部管理与您的 Fragment 的交互,这是非常混乱的。除非您使用片段的生命周期范围,否则您无法从单个协程控制它,但是如果屏幕在完成之前旋转,则提取任务会重新启动。所以我会为此使用一个 ViewModel。
      • 在 ViewModel 中,您应该为您的协程使用viewModelScope,而不是创建一个您永远不会取消的临时 CoroutineScope。当 ViewModel 超出范围时,viewModelScope 将自动取消您的协程。
      • 协程使按顺序执行后台工作变得非常容易。您只需要在单个协程中按顺序调用挂起函数。但要再次做到这一点,您确实需要一个 ViewModel。
      • 对于加载和错误状态有单独的 LiveDatas 是很复杂的。如果使用密封类包装器,Fragment 处理三种可能的状态(加载、错误、有数据)会简单得多。

      将这些放在一起给出以下内容。我真的不知道您的回购在做什么以及如何将Response&lt;GeocodingModel&gt; 转换为List&lt;GeocodingModel&gt;(或为什么),所以我只是使用占位符函数。天气也一样。

      sealed class WeatherState {
          object Loading: WeatherState()
          object Error: WetaherState()
          data class LoadedData(val data: List<SharedWeatherModel>)
      }
      
      class WeatherViewModel constructor(private val repository: WeatherRepository): ViewModel() {
      
          val location = MutableLiveData<List<GeocodingModel>>()
      
          private val API_KEY = „xxxxxxxxxxxxxxxxxxxxxxxxx”
      
          val weather = MutableLiveData<LoadedData>().apply {
                  value = WeatherState.Loading
              }
      
          fun refreshLocation() = viewModelScope.launch {
              weather.value = WeatherState.Loading
              val locationResponse = fetchLocation() //Response<GeocodingModel>
              val locationList = unwrapLocation(location) //List<GeocodingModel>
              location.value = locationList
              val latitude = locationList.get(0).get(0).lat.toString()
              val longitude = locationList.get(0).get(0).lon.toString()
              try {
                  val weatherResponse = fetchWeather(latitude, longitude) //Response<SharedWeatherModel>
                  val weatherList = unwrapWeather(weatherResponse) //List<SharedWeatherModel>
                  weather.value = WeatherState.LoadedData(weatherList)
              } catch (e: Exception) {
                  weather.value = WeatherState.Error
              }        
          }
      
          private suspend fun fetchLocation(): Response<GeocodingModel> {
              return repository.getCoordinates(
                      "Szczecin",
                      API_KEY
                  )
          }
      
          private suspend fun fetchWeather(lat: String, lon: String): Response<SharedWeatherModel> {
                      return repository.getWeather(
                          lat,
                          lon,
                          "minutely,hourly,alerts",
                          "metric",
                          API_KEY
                      )
          }
      }
      

      在您的 Fragment 中,您可以观察 LiveData。天气实时数据将始终具有三种状态之一,因此您只有一个地方可以使用when 语句来处理您的 UI 应该呈现的三种可能方式。

      【讨论】:

        【解决方案3】:

        不参考您的实际代码,只针对问题本身:

        默认情况下,协程内的代码是顺序的。

        scope.launch(Dispatcher.IO) {
            val coordinates = repository.getCoordinates(place)
            val forecast = repository.getForecast(coordinates)
        }
        

        getCoordinates(place) 和 getForecast(coordinates) 都是 suspend 函数,因为它们正在发出网络请求并等待结果。

        在getCoordinates(place) 完成并返回coordinates 之前,getForecast(coordinates) 不会执行。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2010-10-11
          • 1970-01-01
          • 2020-11-07
          • 1970-01-01
          • 2022-01-23
          • 2020-07-05
          • 2015-06-09
          • 1970-01-01
          相关资源
          最近更新 更多