Не можете дождаться завершения работы сопрограммы с помощью join() в котлине?

#kotlin #asynchronous

Вопрос:

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

 suspend fun getRouterCapabilities(): String? = coroutineScope {
        lateinit var routerRtpCapabilities: JSONObject
        val job = launch {
            socket?.emit("getRouterRtpCapabilities", "", Ack { args ->
                routerRtpCapabilities = args[0] as JSONObject
                Log.d(TAG, routerRtpCapabilities!!.getString("error"))
            })
        }
        job.join()
        Log.d(TAG, "$routerRtpCapabilities")
        return@coroutineScope routerRtpCapabilities.toString()
    }
 

В приведенном выше коде я могу сохранить и распечатать значение routerRtpCapabilites внутри функции emit().Но я получил указанную ниже ошибку, когда попытался получить к ней доступ извне функции emit().

котлин.Неинициализированное исключение propertyaccessexception: свойство lateinit routerRtpCapabilities не было инициализировано

А также я не уверен в том, как здесь используются сопрограммы.Пожалуйста, поправьте меня, если я упустил что-то важное.

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

1. Какую библиотеку вы используете для сетевого взаимодействия? Я временно удалил свой ответ, потому что заметил emit , что это функция, основанная на обратном вызове, поэтому мой пример неверен.

2. «io.сокет.клиент. Сокет» Я использовал эту библиотеку для подключений к WebSocket. В данном коде сокет является экземпляром объекта сокета. socket.emit( ) запрашивает конкретное событие(в данном случае это getRouterCapabilities) и отправляет данные (здесь их нет. Вот почему я передал пустую строку в качестве второго параметра) серверу, и ответ сервера получен в виде аргументов. Из аргументов мне нужно получить возможности маршрутизации и вернуть это значение в качестве возвращаемого значения функции. Но я получаю возвращаемое значение как null из-за того, что функция emit() асинхронна, и это то, что мне нужно решить

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

Ответ №1:

Причина, по которой вы job.join() не ждете подтверждения, заключается в том, что emit функция запускает асинхронное действие и немедленно возвращается. Обратный вызов с результатом вызывается позже. Но задание сопрограммы не знает, нужно ли ждать какого-либо обратного вызова, поэтому оно немедленно завершается, прежде чем подтверждение будет получено некоторое время спустя.

Если у вас есть библиотека с асинхронной функцией с обратным вызовом, вы можете преобразовать ее в функцию приостановки, чтобы ее можно было легко использовать в сопрограммах. Вот как вы могли бы преобразовать эту функцию выделения:

 /** Emits the [event] and suspends until acknowledgement is received. Returns the 
acknowledgement arguments. */
suspend fun Socket.awaitEmit(event: String, vararg arg: Any): Array<out Any?> =
    suspendCoroutine { continuation ->
        emit(event, *arg, Ack { args ->
            continuation.resume(args)
        })
    }
 

Некоторые библиотеки, такие как Retrofit и Firebase, поставляются с версиями функций приостановки своих асинхронных функций, и для них потребуется выполнить описанный выше шаг.

Правильная функция приостановки может быть вызвана из других функций приостановки и в сопрограммах, не делая ничего особенного, например, не помещая ее в другую сопрограмму или withContext блок.

 suspend fun getRouterCapabilities(): String? = routerCababilitiesMutex.withLock { 
    val acknowledgement = socket?.awaitEmit("getRouterRtpCapabilities", "") 
    acknowledgement ?: return null // must have null socket
    val result = acknowledgement[0] as? JSONObject
    Log.d(TAG, result?.getString("error") ?: "result is null")
    return result?.toString()
}
 

Его !! следует изменить, потому что это небезопасно.

Чтобы вызвать функцию приостановки из действия, вы должны сделать это в lifecycleScope сопрограмме, например:

 mic.setOnClickListener{ 
    lifecycleScope.launch {
        val routerCap = someOtherClass.getRouterCapabilities()
        // Do stuff with routerCap here.
    }
}
 

Изменить: Как бы я кэшировал значение в вашем классе модели представления. Вы можете использовать мьютекс, чтобы убедиться, что он не извлекается избыточно.

 private var routerCapabilities: String? = null
private val routerCapabilitiesMutex = Mutex()

suspend fun getRouterCapabilities(): String? = routerCapabilitiesMutex.withLock {
    routerCapabilities?.let { return it }
    val acknowledgement = socket?.awaitEmit("getRouterRtpCapabilities", "")
    acknowledgement ?: return null // must have null socket
    val result = acknowledgement[0] as? JSONObject
    Log.d(TAG, result?.getString("error") ?: "result is null")
    result?.toString().also { routerCapabilities = it }
}
 

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

1. Спасибо за ваш ответ. Я хочу вызвать этот метод в onCreate() другого класса при нажатии на микрофон. Чтобы быть точным, я хочу вызвать его на mic.setOnClickListener{ //val routerCap = getRouterCapabilities() }.Можете ли вы дать мне представление о том, как это можно сделать? Спасибо!!

2. Большое спасибо. Это действительно сработало для меня. У меня есть еще одно сомнение. Как я могу использовать значение routerCap вне блока lifecycleScope?

3. На самом деле нет чистого способа сделать это. Вы можете присвоить ему свойство, обнуляемое, но тогда в любом месте, где вам нужно значение, вам придется проверить, является ли оно нулевым, и если оно не равно нулю, запустите другую сопрограмму для его извлечения. Вероятно, самый чистый способ справиться с этим-иметь свойство nullable в классе с функцией suspend и позволить функции suspend возвращать значение в свойстве, если оно еще не было получено. Затем всегда используйте функцию приостановки, чтобы получить значение, и оно быстро вернется, если оно было получено ранее. Но вы всегда использовали lifecycleScope.launch бы это, чтобы получить его.