Моя функция асинхронна, почему я получаю исключение NetworkOnMainThread?

#android #kotlin #asynchronous #exception #networking

#Android #kotlin #асинхронный #исключение #сеть

Вопрос:

Это объект, на котором выполняются все мои вызовы API. Иногда, когда я вызываю эту функцию, я получаю исключение NetworkOnMainThread . Это происходит не каждый раз. Я в замешательстве, потому что я сделал эту функцию асинхронной… почему я все еще получаю это исключение?

 object APICaller{

    private const val apiKey = "API_KEY_HERE"

    //Live Data Objects
    var errorCode = MutableLiveData<Int>()
    var fetchedResponse = MutableLiveData<Response>()


    //Asynchronous network call
    suspend fun networkCall(query: String) = withContext(Dispatchers.Default){

        val apiURL = "API_URL_HERE"

        try{
            //Get response
            val response = OkHttpClient().newCall(Request.Builder().url(apiURL).build()).execute()

            if(response.isSuccessful){
                //UI changes (including changes to LiveData values) must be performed on main thread.
                Handler(Looper.getMainLooper()).post{
                    fetchedResponse.value = response
                }.also{
                    Log.i("Response Succ", response.toString())
                }
            } else {
                Handler(Looper.getMainLooper()).post{
                    errorCode.value =
                        ToastGenerator.REQUEST_ERROR
                }.also{
                    Log.i("Response Fail", response.toString())
                }
            }
            //Catch any thrown network exceptions whilst attempting to contact API
        } catch(e: Exception){
            Handler(Looper.getMainLooper()).post{
                errorCode.value =
                    ToastGenerator.NETWORK_ERROR
            }.also{
                Log.i("Network Fail", e.message.toString())
            }
        }

    }

}
  

Есть три других класса, которые используют возвращаемое значение из функции APICaller networkCall() . Первая — это ViewModel, которая ссылается на нее напрямую.

 class BrowseViewModel: ViewModel() {

    //LiveData Objects
    //Transformations listen to LiveData in APICaller and map it to LiveData in this ViewModel
    var errorCode: LiveData<Int>? = Transformations.map(APICaller.errorCode){ code ->
        return@map code
    }
    var obtainedResponse: LiveData<String> = Transformations.map(APICaller.fetchedResponse){ response ->
        return@map response.body()?.string()
    }

    //Upon a search request, make a network call
    fun request(query: String) {
        GlobalScope.launch{
            APICaller.networkCall(query)
        }
    }

    //Convert API response to GameData object
    fun handleJSONString(jsonString: String, file: String) : List<GameData>{
        return DataTransformer.JSONToGameData(JSONObject(jsonString), JSONObject(file))
    }
}
  

Второй — это фрагмент, который вызывает функцию ViewModel .

 fun request(query: String){
     browseViewModel.request(query)
     progressSpinner?.visibility = View.VISIBLE
}
  

Третье — это действие, которое вызывает функцию фрагмента.

 private fun makeRequest(query: String){
    browseFragment.let{
        supportFragmentManager.beginTransaction().replace(R.id.fragmentContainer, it).commit()
        it.request(query)
    }
}
  

Может ли это быть связано с этими другими функциями?

Любая обратная связь приветствуется. Заранее благодарю вас 🙂

Комментарии:

1. Почему вы включили withContext() . Вы можете изменить его с помощью функции приостановки без withContext и запустить новую сопрограмму при вызове вашей функции. Это может быть лучшим решением

Ответ №1:

Я не совсем понимаю, что может пойти не так в вашем коде, но он определенно может быть намного чище, что, возможно, решит вашу проблему или, по крайней мере, упростит поиск ошибки.

Хорошая стратегия с suspend функциями — спроектировать их так, чтобы они всегда вызывались из главного диспетчера. Тогда вы можете свободно выполнять вызовы пользовательского интерфейса в них и переносить только фоновые части withContext . Таким образом, ваша вышеупомянутая функция может переместить withContext() вниз, чтобы только обернуть execute() вызов, и все ваше использование обработчика может быть удалено. (Кстати, это было бы более чистым использованием withContext(Dispatchers.Main) .)

Однако Retrofit уже предоставляет версию функции приостановки выполнения вызова, поэтому вам даже не нужна withContext оболочка. Просто используйте await() вместо execute() , и ваша функция приостановки рухнет до:

 suspend fun networkCall(query: String) {

    val apiURL = "API_URL_HERE"

    try{
        val response = OkHttpClient().newCall(Request.Builder().url(apiURL).build()).await()
        if (response.isSuccessful){
            Log.i("Response Succ", response.toString())
            fetchedResponse.value = response
        } else {
            Log.i("Response Fail", response.toString())
            errorCode.value = ToastGenerator.REQUEST_ERROR
        }
    } catch(e: Exception){
        Log.i("Network Fail", e.message.toString())
        errorCode.value = ToastGenerator.NETWORK_ERROR
    }

}
  

И затем вместо использования GlobalScope вы должны использовать viewModelScope или lifecycleScope , чтобы ваши сопрограммы не пропускали компоненты пользовательского интерфейса. И функция await() приостановки выше поддерживает отмену, поэтому, если, например, ваша ViewModel будет уничтожена из-за того, что связанный фрагмент или действие выходят за рамки, ваш сетевой вызов будет автоматически отменен для вас.

 fun request(query: String) {
    viewModelScope.launch {
        APICaller.networkCall(query)
    }
}
  

Если проблема не устранена, внимательно изучите трассировку стека, чтобы увидеть, где вы могли допустить ошибку.