📜 ⬆️ ⬇️

Do not use lambdas as listeners in Kotlin

Hi, Habr! I present to your attention the translation of the article “ Don't use lambdas as listeners in Kotlin” by Alex Gherschon

From the translator : Kotlin is a very powerful language that allows you to write code more concisely and faster. But, lately, there have been too many articles that describe the good sides of the language, thinking about the pitfalls, and they are, because the language introduces new designs that are a black box for a beginner. This article, which is a translation, discusses the use of lambdas as listeners on Android. It will help not to step on the same rake, which the author also stepped on, because, ultimately, the specificity of the platform does not go away when the language is changed.

I stumbled upon this problem in my first application I write on Kotlin, and it drove me crazy!

Introduction


I use AudioFocus in a podcast listening application. When a user wants to listen to an episode, you need to request audio focus by passing the OnAudioFocusChangeListener implementation (because we can lose audio focus when playing if the user uses another application that also requires audio focus):
')
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 } 

In this listener, we want to handle various states:

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

When the episode is over or the user stops it, you need to release the audio focus :

 private fun abandonAudioFocus(): Boolean { Log.d(TAG, "abandonAudioFocus() called") val focusRequest: Int = audioManager.abandonAudioFocus(onAudioFocusChange) return focusRequest == AudioManager.AUDIOFOCUS_REQUEST_GRANTED } 

Road to madness


With my passion for new things, I decided to implement the listener, onAudioFocusChange , using lambda. I do not remember whether it was suggested by IntelliJ IDEA or not, but, in any case, it was declared as follows:

 private lateinit var onAudioFocusChange: (focusChange: Int) -> Unit 

In onCreate (), this variable is assigned lambda:

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

And everything worked well, because Now we can request audio focus, which will stop other applications (for example, Spotify) and play our episode.

The release of the audio focus, too, seemed to work, because I received AUDIOFOCUS_REQUEST_GRANTED as a result when calling the AudioManager class abandonAudioFocus class:

 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 

But as soon as we want to request the audio focus again, we immediately lose it and receive the AUDIOFOCUS_LOSS event:

 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 

Why do we lose it as soon as requested? What is going on?

Behind the scenes


The best tool to understand the problem is the Kotlin Bytecode bytecode viewer :

image

image

Let's see what is assigned to our onAudioFocusChange variable:

 this.onAudioFocusChange = (Function1)null.INSTANCE; 

You can see that lambdas are converted to classes of the form FunctionN, where N is the number of parameters. The concrete implementation is hidden here, and we need another tool to view it, but that’s another story.

Let's see the OnAudioFocusChangeListener implementation:

 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(...)"); } } 

And now let's check how it is used. RequestAudioFocus method:

 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; } 

The abandonAudioFocus method:

 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; } 

You may have noticed the problem line in both places:

 var10001 = new MainActivityKt$sam$OnAudioFocusChangeListener$4186f324((Function1)var2); 

In fact, the following happens: our lambda / Function1 is initialized in onCreate (), but each time we pass it as a SAM to a function, it is wrapped in a new instance of the class that implements the listener interface, which means that two instances will be created The listener and AudioManager API cannot delete when calling abandonAudioFocus () the listener that was created earlier and used when calling requestAudioFocus () . Since the original listener is never deleted, we receive the event AUDIO_FOCUS_LOSS in it.

The right approach


Students must remain anonymous internal classes, so here’s the correct way to define it:

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

Now the onAudioFocusChange variable refers to the same listener instance, which is correctly passed to the AudioManager class requestAudioFocus and abandonAudioFocus methods . Fine!

Code example


You can look at the generated bytecode and see the problem personally in this repository on GitHub .

Conclusion (but not quite)


With great power comes great responsibility. Do not use lambdas instead of anonymous inner classes for listeners. I got an important lesson and I hope that he, too, has benefited you.

P.S


As one of the readers pointed out in the comments (thanks, Pavlo!) We can declare a lambda as follows and everything will work correctly:

 onAudioFocusChange = AudioManager.OnAudioFocusChangeListener { focusChange: Int -> Log.d(TAG, "In onAudioFocusChange focus changed to = $focusChange") // do stuff } 

PostScript Explanation


Is lateinit to blame ?


Some readers claimed that the problem was in declaring a listener with the lateinit modifier. To check whether it is lateinit or not, let's try to implement a lambda with this modifier and without it and look at the result.

To remind what we are talking about, here is the code of these two lambdas:

 // 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 } 

With 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)); 


Our lambda is wrapped inside a class that implements the interface (SAM Transform), but we don’t own a reference to the converted class, which is the problem.

Without 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)); 


It can be seen that the same problem without lateinit , so we can not blame this modifier.

Recommended way


To fix the problem, I recommend using an anonymous inner class:

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

Which translates to the following in 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); 

The anonymous class implements the necessary interface and we have a single instance (the compiler does not need to do the SAM transformation , since there are no lambdas here). Fine!

The best way


The most concise way is to still declare a lambda and use what the documentation calls the conversion method :

 private val onAudioFocusChangeListener4 = AudioManager.OnAudioFocusChangeListener { focusChange: Int -> Log.d(TAG, "In onAudioFocusChangeListener3 focus changed to = $focusChange") // do some stuff } 

This indicates to the compiler that this is the type to use when converting SAM . Java Result Code:

 // 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); 

Conclusion (now completely)


As Roman Dawydkin remarkably noted in Slack :

You can use a lambda as a listener only if you use it once.

Not a problem if lambda is used in a functional style or as a callback function. A problem manifests itself only when it is used as a listener in an API written in Java, which expects the same instance in the Observer pattern. If the API is written in Kotlin, then there is no SAM conversion , respectively, and there is no problem. Someday all API will be like that!

I hope that this topic is now very clear to everyone.

I would like to thank Rhaquel Gherschon for reading and Christophe Beyls for the comments on this article!

Hooray!

From the translator : This is just one of the pitfalls. Another example is incorrect brackets in the RxJava + SAM + Kotlin combination.

Source: https://habr.com/ru/post/342460/


All Articles