Thank you to everyone of you who read the original blogpost and even more to those who commented on it or on Reddit!

As mentioned in the comments we actually can declare a lambda this way:

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

Is Lateinit guilty or not?

Some argued that the problem is the declaration of the listener using the lateinit keyword. To check if lateinit is indeed guilty, let’s reproduce the same lambda with and without lateinit and see how it is used.

To remind us what we’re talking about, here’s the code of the two lambdas in Kotlin:

// 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 $ 4186 f324 = new MainActivityKt$sam$OnAudioFocusChangeListener $ 4186 f324 ( function1 ); } else { Object obj = function1 ; } Log . d ( MainActivity . Companion . getTAG (), "granted = " + ( access$getAudioManager$p . requestAudioFocus (( OnAudioFocusChangeListener ) mainActivityKt$sam$OnAudioFocusChangeListener $ 4186 f324 , 3 , 1 ) == 1 ));

Our lambda / function literal is wrapped inside a class implementing the interface (SAM Conversion) but we do not hold a reference to the converted class, which is the whole issue.

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 $ 4186 f324 = new MainActivityKt$sam$OnAudioFocusChangeListener $ 4186 f324 ( function1 ); } else { Object obj = function1 ; } Log . d ( MainActivity . Companion . getTAG (), "granted = " + ( access$getAudioManager$p . requestAudioFocus (( OnAudioFocusChangeListener ) mainActivityKt$sam$OnAudioFocusChangeListener $ 4186 f324 , 3 , 1 ) == 1 ));

Same issue without lateinit, so we hold no charges against it.

The preferable way

To fix the issue, I recommended 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 expected interface and the same instance is used (the compiler does not need to use a SAM Conversion as no lambdas are involved). Neat!

The best way

The most concise way is to actually declare the lambda and use what the documentation calls an adapter function:

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 doing a SAM Conversion on it. It translates to the following in 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 );

Conclusion

As perfectly put by Roman Dawydkin on Slack:

You can use lambda as a listener if you create only one instance of it

It’s not an issue if the lambda is used in a functional way or as a callback. The issue occurs only when used as listeners with an API written in Java which expects the same object reference in an observer design pattern. If the API would have been written in Kotlin there would have been no SAM Conversion involved thus no issue. We’ll get there one day!

I hope the subject is now crystal clear for everyone!

I’d like to thank Rhaquel Gherschon for proofreading and Christophe Beyls for giving me feedback on this article!

Cheers!

Please enable JavaScript to view the comments powered by Disqus.