Не Используйте Лямбды В Качестве Слушателей В Котлине

Привет, Хабр! Представляю вашему вниманию перевод статьи Не используйте лямбды в качестве слушателей в Котлине Алекс Гершон От переводчика : 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 сорт АудиоМенеджер .

Большой!

Пример кода

Вы можете посмотреть сгенерированный байт-код и увидеть проблему лично.

в этом репозитории GitHub .



Вывод (но не совсем)

С большой властью приходит большая ответственность.

Не используйте лямбды вместо анонимных внутренних классов для прослушивателей.

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



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

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.