androidkotlinkotlin-coroutinescoroutinescopekotlin-android

Async requests in Kotlin Android


I often get an error android.os.NetworkOnMainThreadException, when I try get info from some api. I know that this problem is related to the main android thread, but I don't understand how to solve it - coroutines, async okhttp, or both? P.S I have a bad eng, sorry.

My code:

MainAtivity.kt

class MainActivity: AppCompatActivity(), Alert {
    private lateinit var binding: ActivityMainBinding
    lateinit var api: ApiWeather
    var okHttpClient: OkHttpClient = OkHttpClient()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        api = ApiWeather(okHttpClient)

        binding.buttonGetWeather.setOnClickListener {
            val cityInput = binding.textInputCity.text.toString()
            if (cityInput.isEmpty()) {
                errorAlert(this, "...").show()
            } else {
                val city = "${cityInput.lowercase()}"
                val limit = "1"
                val appId = "key"
                val urlGeocoding = "http://api.openweathermap.org/geo/1.0/direct?" +
                        "q=$city&limit=$limit&appid=$appId"

                var status = false
                val coordinates: MutableMap<String, Double> = mutableMapOf()
                val job1: Job = lifecycleScope.launch {
                        val geo = api.getGeo(urlGeocoding)
                        if (geo != null) {
                            coordinates["lat"] = geo.lat
                            coordinates["lon"] = geo.lon
                            status = true
                        } else {
                            status = false
                        }
                }
                val job2: Job = lifecycleScope.launch {
                    job1.join()
                    when(status) {
                        false -> {
                            binding.textviewTempValue.text = ""
                            errorAlert(this@MainActivity, "...").show()
                        }
                        true -> {
                            val urlWeather = "https://api.openweathermap.org/data/2.5/weather?" +
                                    "lat=${coordinates["lat"]}&lon=${coordinates["lon"]}&units=metric&appid=${appId}"
                            val weather = api.getTemp(urlWeather)
                            binding.textviewTempValue.text = weather.main.temp.toString()
                        }
                    }
                }
            }
        }
    }
}

Api.kt

class ApiWeather(cl: OkHttpClient) {
    private val client: OkHttpClient

    init {
        client = cl
    }

    suspend fun getGeo(url: String): GeocodingModel? {
        val request: Request = Request.Builder()
            .url(url)
            .build()
        val responseStr = client.newCall(request).await().body?.string().toString()
        val json = Json {
            ignoreUnknownKeys = true
        }
        return try {
            json.decodeFromString<List<GeocodingModel>>(responseStr)[0]
        } catch (e: Exception) {
            return null
        }
    }

    suspend fun getTemp(url: String): DetailWeatherModel {
        val request: Request = Request.Builder()
            .url(url)
            .build()
        val responseStr = client.newCall(request).await().body?.string().toString()
        val json = Json {
            ignoreUnknownKeys = true
        }
        return json.decodeFromString<DetailWeatherModel>(responseStr)
    }
}

Solution

  • The problem is that api.getGeo(urlGeocoding) runs in the current thread. lifecycleScope.launch {} by default has Dispatchers.Main context, so calling api function will run on the Main Thread. To make it run in background thread you need to switch context by using withContext(Dispatchers.IO). It will look like the following:

    lifecycleScope.launch {
          val geo = withContext(Dispatchers.IO) { api.getGeo(urlGeocoding) }
          if (geo != null) {
               coordinates["lat"] = geo.lat
               coordinates["lon"] = geo.lon
               status = true
          } else {
               status = false
          }
    
          when(status) { ... }           
    }