Привет, Хабр! Представляю вашему вниманию перевод статьи Не используйте лямбды в качестве слушателей в Котлине Алекс Гершон От переводчика : Kotlin — очень мощный язык, позволяющий писать код более лаконично и быстрее.
Но в последнее время появилось слишком много статей, которые описывают хорошие стороны языка, умалчивая о подводных камнях, но они существуют, потому что в языке вводятся новые конструкции, которые являются черным ящиком для новичка.
В этой переводной статье рассматривается использование лямбда-выражений в качестве прослушивателей в Android. Это поможет вам не наступить на те же ошибки, что и автор, ведь в конечном итоге специфика платформы не исчезает при смене языка.
Я столкнулся с этой проблемой в своем первом приложении, которое пишу на Kotlin, и это свело меня с ума!
Введение
я использую АудиоФокус в приложении для прослушивания подкастов.Когда пользователь хочет прослушать выпуск, ему необходимо запросить аудиофокус , передавая реализацию OnAudioFocusChangeListener (потому что мы можем потерять фокус звука во время воспроизведения, если пользователь использует другое приложение, которое также требует фокуса звука):
В этом слушателе мы хотим обрабатывать различные состояния:private fun requestAudioFocus(): Boolean { Log.d(TAG, "requestAudioFocus() called") val focusRequest: Int = audioManager.requestAudioFocus(onAudioFocusChange, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) return focusRequest == AudioManager.AUDIOFOCUS_REQUEST_GRANTED }
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> TODO("resume playing")
AudioManager.AUDIOFOCUS_LOSS -> TODO("abandon focus and stop playing")
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> TODO("pause but keep focus")
}
Когда эпизод закончен или пользователь останавливает его, вы должны отпустить аудиофокус :
private fun abandonAudioFocus(): Boolean {
Log.d(TAG, "abandonAudioFocus() called")
val focusRequest: Int = audioManager.abandonAudioFocus(onAudioFocusChange)
return focusRequest == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
}
Дорога к безумию
Со своей страстью к новому я решил реализовать слушателя, onAudioFocusChange , используя лямбду.
Я не помню, было ли это предложено IntelliJ IDEA или нет, но в любом случае было объявлено так: private lateinit var onAudioFocusChange: (focusChange: Int) -> Unit
В onCreate() этой переменной присвоена лямбда: onAudioFocusChange = { focusChange: Int ->
Log.d(TAG, "In onAudioFocusChange focus changed to = $focusChange")
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> TODO("resume playing")
AudioManager.AUDIOFOCUS_LOSS -> TODO("abandon focus and stop playing")
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> TODO("pause but keep focus")
}
}
И все работало хорошо, потому что.
Теперь мы можем запросить фокусировку звука, которая остановит другие приложения (например, Spotify) и воспроизведет наш эпизод. Отключение аудиофокуса, похоже, тоже сработало, потому что.
я получил AUDIOFOCUS_REQUEST_GRANTED как результат при вызове метода оставитьAudioFocus сорт АудиоМенеджер : 11-04 16:08:14.610 D/MainActivity: requestAudioFocus() called
11-04 16:08:14.618 D/AudioManager: requestAudioFocus status : 1
11-04 16:08:14.619 D/MainActivity: granted = true
11-04 16:09:34.519 D/MainActivity: abandonAudioFocus() called
11-04 16:09:34.521 D/MainActivity: granted = true
Но как только мы хотим снова запросить аудиофокус, мы сразу его теряем и получаем событие АУДИОФОКУС_ПОТЕРЯ : 11-04 16:17:38.307 D/MainActivity: requestAudioFocus() called
11-04 16:17:38.312 D/AudioManager: requestAudioFocus status : 1
11-04 16:17:38.312 D/MainActivity: granted = true
11-04 16:17:38.321 D/AudioManager: AudioManager dispatching onAudioFocusChange(-1)
// for MainActivityKt$sam$OnAudioFocusChangeListener$4186f324$828aa1f
11-04 16:17:38.322 D/MainActivity: In onAudioFocusChange focus changed to = -1
Почему мы теряем его, как только запросили? Что происходит?
За кулисами
Лучший инструмент для понимания проблемы — программа просмотра байт-кода.
Котлин Байт-код :
Давайте посмотрим, что присвоено нашей переменной onAudioFocusChange : this.onAudioFocusChange = (Function1)null.INSTANCE;
Вы можете заметить, что лямбды преобразуются в классы вида FunctionN, где N — количество параметров.
Конкретная реализация здесь скрыта, и для ее просмотра вам понадобится другой инструмент, но это уже другая история.
Давайте посмотрим реализацию OnAudioFocusChangeListener : final class MainActivityKt$sam$OnAudioFocusChangeListener$4186f324 implements OnAudioFocusChangeListener {
// $FF: synthetic field
private final Function1 function;
MainActivityKt$sam$OnAudioFocusChangeListener$4186f324(Function1 var1) {
this.function = var1;
}
// $FF: synthetic method
public final void onAudioFocusChange(int focusChange) {
Intrinsics.checkExpressionValueIsNotNull(this.function.invoke(Integer.valueOf(focusChange)), "invoke(.
)");
}
}
Теперь проверим, как оно используется.
Метод запросAudioFocus : private final boolean requestAudioFocus() {
Log.d(Companion.getTAG(), "requestAudioFocus() called");
(.
)
Object var10001 = this.onAudioFocusChange;
if(this.onAudioFocusChange == null) {
Intrinsics.throwUninitializedPropertyAccessException("onAudioFocusChange");
}
if(var10001 != null) {
Object var2 = var10001;
var10001 = new MainActivityKt$sam$OnAudioFocusChangeListener$4186f324((Function1)var2);
}
int focusRequest = var10000.requestAudioFocus((OnAudioFocusChangeListener)var10001, 3, 1);
Log.d(Companion.getTAG(), "granted = " + (focusRequest == 1));
return focusRequest == 1;
}
Метод оставитьAudioFocus : private final boolean abandonAudioFocus() {
Log.d(Companion.getTAG(), "abandonAudioFocus() called");
(.
)
Object var10001 = this.onAudioFocusChange;
if(this.onAudioFocusChange == null) {
Intrinsics.throwUninitializedPropertyAccessException("onAudioFocusChange");
}
if(var10001 != null) {
Object var2 = var10001;
var10001 = new MainActivityKt$sam$OnAudioFocusChangeListener$4186f324((Function1)var2);
}
int focusRequest = var10000.abandonAudioFocus((OnAudioFocusChangeListener)var10001);
Log.d(Companion.getTAG(), "granted = " + (focusRequest == 1));
return focusRequest == 1;
}
Возможно, вы заметили проблемную строку в обоих местах: var10001 = new MainActivityKt$sam$OnAudioFocusChangeListener$4186f324((Function1)var2);
На самом деле происходит то, что наша лямбда/функция1 инициализируется в onCreate(), но каждый раз, когда мы передаем ее как СЭМ в функцию, он оборачивается новым экземпляром класса, реализующего интерфейс прослушивателя, а это означает, что будут созданы два экземпляра прослушивателя и АудиоМенеджер API невозможно удалить при вызове отказаться от AudioFocus() прослушиватель, который был создан ранее и использовался в вызове запросАудиоФокус() .
Поскольку исходный слушатель никогда не удаляется, мы получаем в нем событие AUDIO_FOCUS_LOSS .
Правильный подход
Слушатели должны оставаться анонимными внутренними классами, поэтому вот правильный способ их определения: private lateinit var onAudioFocusChange: AudioManager.OnAudioFocusChangeListener
onAudioFocusChange = object : AudioManager.OnAudioFocusChangeListener {
override fun onAudioFocusChange(focusChange: Int) {
Log.d(TAG, "In onAudioFocusChange (${this.toString().
substringAfterLast("@")}), focus changed to = $focusChange")
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> TODO("resume playing")
AudioManager.AUDIOFOCUS_LOSS -> TODO("abandon focus and stop playing")
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> TODO("pause but keep focus")
}
}
}
Теперь переменная onAudioFocusChange относится к тому же экземпляру прослушивателя, который правильно передается методам запросAudioFocus И отказаться от AudioFocus сорт АудиоМенеджер .
Большой!
Пример кода
Вы можете посмотреть сгенерированный байт-код и увидеть проблему лично.
Вывод (но не совсем)
С большой властью приходит большая ответственность.Не используйте лямбды вместо анонимных внутренних классов для прослушивателей.
Я усвоил важный урок и надеюсь, что вы тоже извлекли из него пользу.
P.S.
Как заметил в комментариях один читатель (спасибо, Павел!), мы можем объявить лямбду вот так, и все будет работать правильно: onAudioFocusChange = AudioManager.OnAudioFocusChangeListener { focusChange: Int ->
Log.d(TAG, "In onAudioFocusChange focus changed to = $focusChange")
// do stuff
}
Пояснение к постскриптуму
Это моя вина? Латинит ?
Некоторые читатели утверждают, что проблема в объявлении слушателя с модификатором Латинит .
Чтобы проверить, виноват ли он поздноинит или нет, попробуем реализовать лямбду с этим модификатором и без него и посмотрим на результат.
Чтобы напомнить вам, о чем мы говорим, вот код этих двух лямбд: // with lateinit
private lateinit var onAudioFocusChangeListener1: (focusChange: Int) -> Unit
// without lateinit
private val onAudioFocusChangeListener2: (focusChange: Int) -> Unit = { focusChange: Int ->
Log.d(TAG, "In onAudioFocusChangeListener2 focus changed to = $focusChange")
// do some stuff
}
// in onCreate()
onAudioFocusChangeListener1 = { focusChange: Int ->
Log.d(TAG, "In onAudioFocusChangeListener1 focus changed to = $focusChange")
// do some stuff
}
С lateinit (onAudioFocusChangeListener1) // Declaration
private Function1<? super Integer, Unit> onAudioFocusChangeListener1;
// in onCreate()
this.onAudioFocusChangeListener1 = MainActivity$onCreate$1.INSTANCE;
// Class implementation
final class MainActivity$onCreate$1 extends Lambda implements Function1<Integer, Unit> {
public static final MainActivity$onCreate$1 INSTANCE = new MainActivity$onCreate$1();
MainActivity$onCreate$1() {
super(1);
}
public final void invoke(int focusChange) {
Log.d(MainActivity.Companion.getTAG(), "In onAudioFocusChangeListener1 focus changed to = " + focusChange);
}
}
// In onCreate(), a button uses a SAM converted lambda to call the AudioManager API
Function1 listener = this.onAudioFocusChangeListener1;
((Button) findViewById(C0220R.id.obtain)).
setOnClickListener(new MainActivity$onCreate$2(this, listener));
// Inside MainActivity$onCreate$2 the call to the AudioManager API
if (function1 != null) {
mainActivityKt$sam$OnAudioFocusChangeListener$4186f324 = new MainActivityKt$sam$OnAudioFocusChangeListener$4186f324(function1);
} else {
Object obj = function1;
}
Log.d(MainActivity.Companion.getTAG(), "granted = " + (access$getAudioManager$p.requestAudioFocus((OnAudioFocusChangeListener) mainActivityKt$sam$OnAudioFocusChangeListener$4186f324, 3, 1) == 1));
Наша лямбда заключена внутри класса, реализующего интерфейс (преобразование SAM), но у нас нет ссылки на преобразованный класс, и в этом проблема.
Без lateinit (onAudioFocusChangeListener2) // Declaration of the lambda
private final Function1<Integer, Unit> onAudioFocusChangeListener2 = MainActivity$onAudioFocusChangeListener2$1.INSTANCE;
// Class implementation
final class MainActivity$onAudioFocusChangeListener2$1 extends Lambda implements Function1<Integer, Unit> {
public static final MainActivity$onAudioFocusChangeListener2$1 INSTANCE = new MainActivity$onAudioFocusChangeListener2$1();
MainActivity$onAudioFocusChangeListener2$1() {
super(1);
}
public final void invoke(int focusChange) {
Log.d(MainActivity.Companion.getTAG(), "In onAudioFocusChangeListener1 focus changed to = " + focusChange);
}
}
// In onCreate(), a button uses a SAM converted lambda to call the AudioManager API
Function1 listener = this.onAudioFocusChangeListener2;
((Button) findViewById(C0220R.id.obtain)).
setOnClickListener(new MainActivity$onCreate$2(this, listener));
// Inside MainActivity$onCreate$2 the call to the AudioManager API
if (function1 != null) {
mainActivityKt$sam$OnAudioFocusChangeListener$4186f324 = new MainActivityKt$sam$OnAudioFocusChangeListener$4186f324(function1);
} else {
Object obj = function1;
}
Log.d(MainActivity.Companion.getTAG(), "granted = " + (access$getAudioManager$p.requestAudioFocus((OnAudioFocusChangeListener) mainActivityKt$sam$OnAudioFocusChangeListener$4186f324, 3, 1) == 1));
Видно, что та же проблема существует и без Латинит , поэтому мы не можем винить этот модификатор.
Рекомендуемый метод
Чтобы решить проблему, я рекомендую использовать анонимный внутренний класс: private val onAudioFocusChangeListener3: AudioManager.OnAudioFocusChangeListener = object : AudioManager.OnAudioFocusChangeListener {
override fun onAudioFocusChange(focusChange: Int) {
Log.d(TAG, "In onAudioFocusChangeListener2 focus changed to = $focusChange")
// do some stuff
}
}
Что переводится на Java следующим образом:
// declaration
private final OnAudioFocusChangeListener onAudioFocusChangeListener3 = new MainActivity$onAudioFocusChangeListener3$1();
// class definition
public final class MainActivity$onAudioFocusChangeListener3$1 implements OnAudioFocusChangeListener {
MainActivity$onAudioFocusChangeListener3$1() {
}
public void onAudioFocusChange(int focusChange) {
Log.d(MainActivity.Companion.getTAG(), "In onAudioFocusChangeListener2 focus changed to = " + focusChange);
}
}
// In onCreate(), a button uses a SAM converted lambda to call the AudioManager API
OnAudioFocusChangeListener listener = this.onAudioFocusChangeListener3;
((Button) findViewById(C0220R.id.obtain)).
setOnClickListener(new MainActivity$onCreate$2(this, listener));
// Inside MainActivity$onCreate$2 the call to the AudioManager API
Log.d(MainActivity.Companion.getTAG(), "Calling AudioManager.requestAudioFocus()");
int focusRequest = MainActivity.access$getAudioManager$p(this.this$0).
requestAudioFocus(this.$listener, 3, 1);
Анонимный класс реализует необходимый интерфейс, и у нас есть единственный экземпляр (компилятору не нужно делать Конверсия ЗРК , потому что здесь нет лямбд).
Большой!
Лучший путь
Самый лаконичный способ — объявить лямбду и использовать то, что называется в документации.
метод преобразования : private val onAudioFocusChangeListener4 = AudioManager.OnAudioFocusChangeListener { focusChange: Int ->
Log.d(TAG, "In onAudioFocusChangeListener3 focus changed to = $focusChange")
// do some stuff
}
Это сообщает компилятору, что этот тип следует использовать, когда Конверсия ЗРК .
Результирующий код на Java: // declaration
private final OnAudioFocusChangeListener onAudioFocusChangeListener4 = MainActivity$onAudioFocusChangeListener4$1.INSTANCE;
// Class definition
final class MainActivity$onAudioFocusChangeListener4$1 implements OnAudioFocusChangeListener {
public static final MainActivity$onAudioFocusChangeListener4$1 INSTANCE = new MainActivity$onAudioFocusChangeListener4$1();
MainActivity$onAudioFocusChangeListener4$1() {
}
public final void onAudioFocusChange(int focusChange) {
Log.d(MainActivity.Companion.getTAG(), "In onAudioFocusChangeListener3 focus changed to = " + focusChange);
}
}
// In onCreate(), a button uses a SAM converted lambda to call the AudioManager API
OnAudioFocusChangeListener listener = this.onAudioFocusChangeListener4;
((Button) findViewById(C0220R.id.obtain)).
setOnClickListener(new MainActivity$onCreate$2(this, listener)); // Inside MainActivity$onCreate$2 the call to the AudioManager API Log.d(MainActivity.Companion.getTAG(), "Calling AudioManager.requestAudioFocus()"); int focusRequest = MainActivity.access$getAudioManager$p(this.this$0).
requestAudioFocus(this.$listener, 3, 1);
Вывод (теперь полностью)
Как приятно отметить Роман Давыдкин В Слабый :Вы можете использовать лямбду в качестве прослушивателя, только если используете ее один раз.Не проблема, если лямбда используется в функциональном стиле или как функция обратного вызова.
Проблема появляется только тогда, когда он используется в качестве прослушивателя в API, написанный на Java который ожидает тот же экземпляр в шаблоне Observer. Если API написано на Котлине, то нет. Конверсии SAM , соответственно проблемы нет. Когда-нибудь все API будут такими! Надеюсь, эта тема теперь всем понятна.
Я хотел бы поблагодарить Ракель Гершон за корректуру и Кристоф Бейлс за ваши комментарии к этой статье! Ура! От переводчика : Это лишь один из подводных камней.
Другой пример - неправильные круглые скобки в комбинации RxJava + SAM + Kotlin Теги: #перевод #Android #Kotlin #разработка под Android #Kotlin
-
Коран, Хар Гобинд
19 Oct, 24 -
Не Пропустите Одную Рекламу...
19 Oct, 24 -
Опять Мошенники. Сейчас В Ирландии
19 Oct, 24 -
Идеальный Смартфон, Какой Он?
19 Oct, 24 -
Черная Дыра Поглощает Звезду (Рендер)
19 Oct, 24 -
Именованные Параметры Повышения
19 Oct, 24