Async requests in Kotlin Android

2k views Asked by At

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)
    }
}
3

There are 3 answers

0
Sergio On BEST ANSWER

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) { ... }           
}
1
Steyrix On

You are already using coroutines. The problem is that lifecycleScope is tied to main thread. You want to replace it with GlobalScope or coroutineScope (latter is better in terms of complex project, but I assume you are writing pet-project now, so GlobalScope.launch will be fine)

0
Ivo On

you should replace

lifecycleScope.launch{

with

lifecycleScope.launch(Dispatchers.IO){