144

Don't use lambdas as listeners in Kotlin - Update

 6 years ago
source link: http://galex.co.il/2017/11/07/Dont_Use_lambdas_as_listeners_in_Kotlin_Update.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

The TLDR; Update

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
}

The Longer Update

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

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!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK