I will use my code to describe the issue. I want to add a clipboard change listener when a service starts, and remove it when the service is destroyed. My service:
class CopyListenerService : Service() {
override fun onBind(p0: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
clipboardManager.addPrimaryClipChangedListener(onPrimaryClipChangedListener)
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
clipboardManager.removePrimaryClipChangedListener(onPrimaryClipChangedListener)
}
val clipboardManager by lazy { getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager }
val onPrimaryClipChangedListener = {
Log.d(TAG, "Clip Item count: " + clipboardManager.primaryClip.itemCount)
Unit
}
companion object {
val TAG = "CopyListenerService"
}
}
And the problem is that clipboardManager.removePrimaryClipChangedListener()
fails to remove the listener.
Show the bytecode and decompile the source, I find that onPrimaryClipChangedListener
is translated into a Function0
object and a new class CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3
, which implements OnPrimaryClipChangedListener
and wraps the onPrimaryClipChangedListener
object, is generated. When I call clipboardManager.addPrimaryClipChangedListener(onPrimaryClipChangedListener)
, it actually creates a new instance of CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3
. When removing the listener, it creates another new instance, and they are definitely not the same one, though they wraps the same onPrimaryClipChangedListener
object.
So is it the intended behavior of Kotlin? If so, what’s the best practice to avoid such issue?
Here’s the decompiled java code:
// CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3.java
package com.perqin.copyshare;
import android.content.ClipboardManager.OnPrimaryClipChangedListener;
import kotlin.Metadata;
import kotlin.jvm.functions.Function0;
import kotlin.jvm.internal.Intrinsics;
@Metadata(
mv = {1, 1, 6},
bv = {1, 0, 1},
k = 3
)
final class CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3 implements OnPrimaryClipChangedListener {
// $FF: synthetic field
private final Function0 function;
CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3(Function0 var1) {
this.function = var1;
}
// $FF: synthetic method
public final void onPrimaryClipChanged() {
Intrinsics.checkExpressionValueIsNotNull(this.function.invoke(), "invoke(...)");
}
}
// CopyListenerService.java
package com.perqin.copyshare;
import android.app.Service;
import android.content.ClipboardManager;
import android.content.Intent;
import android.content.ClipboardManager.OnPrimaryClipChangedListener;
import android.os.IBinder;
import android.util.Log;
import kotlin.Lazy;
import kotlin.LazyKt;
import kotlin.Metadata;
import kotlin.TypeCastException;
import kotlin.Unit;
import kotlin.jvm.functions.Function0;
import kotlin.jvm.internal.DefaultConstructorMarker;
import kotlin.jvm.internal.PropertyReference1Impl;
import kotlin.jvm.internal.Reflection;
import kotlin.reflect.KProperty;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@Metadata(
mv = {1, 1, 6},
bv = {1, 0, 1},
k = 1,
d1 = {"\u00006\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0005\n\u0002\u0018\u0002\n\u0002\u0010\u0002\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\b\n\u0002\b\u0005\u0018\u0000 \u00182\u00020\u0001:\u0001\u0018B\u0005¢\u0006\u0002\u0010\u0002J\u0014\u0010\u000e\u001a\u0004\u0018\u00010\u000f2\b\u0010\u0010\u001a\u0004\u0018\u00010\u0011H\u0016J\b\u0010\u0012\u001a\u00020\u000bH\u0016J\"\u0010\u0013\u001a\u00020\u00142\b\u0010\u0015\u001a\u0004\u0018\u00010\u00112\u0006\u0010\u0016\u001a\u00020\u00142\u0006\u0010\u0017\u001a\u00020\u0014H\u0016R\u001b\u0010\u0003\u001a\u00020\u00048FX\u0086\u0084\u0002¢\u0006\f\n\u0004\b\u0007\u0010\b\u001a\u0004\b\u0005\u0010\u0006R\u0017\u0010\t\u001a\b\u0012\u0004\u0012\u00020\u000b0\n¢\u0006\b\n\u0000\u001a\u0004\b\f\u0010\r¨\u0006\u0019"},
d2 = {"Lcom/perqin/copyshare/CopyListenerService;", "Landroid/app/Service;", "()V", "clipboardManager", "Landroid/content/ClipboardManager;", "getClipboardManager", "()Landroid/content/ClipboardManager;", "clipboardManager$delegate", "Lkotlin/Lazy;", "onPrimaryClipChangedListener", "Lkotlin/Function0;", "", "getOnPrimaryClipChangedListener", "()Lkotlin/jvm/functions/Function0;", "onBind", "Landroid/os/IBinder;", "p0", "Landroid/content/Intent;", "onDestroy", "onStartCommand", "", "intent", "flags", "startId", "Companion", "production sources for module app"}
)
public final class CopyListenerService extends Service {
@NotNull
private final Lazy clipboardManager$delegate = LazyKt.lazy((Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
return this.invoke();
}
@NotNull
public final ClipboardManager invoke() {
Object var10000 = CopyListenerService.this.getSystemService("clipboard");
if(var10000 == null) {
throw new TypeCastException("null cannot be cast to non-null type android.content.ClipboardManager");
} else {
return (ClipboardManager)var10000;
}
}
}));
@NotNull
private final Function0 onPrimaryClipChangedListener = (Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}
public final void invoke() {
Log.d(CopyListenerService.Companion.getTAG(), "Clip Item count: " + CopyListenerService.this.getClipboardManager().getPrimaryClip().getItemCount());
}
});
@NotNull
private static final String TAG = "CopyListenerService";
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(CopyListenerService.class), "clipboardManager", "getClipboardManager()Landroid/content/ClipboardManager;"))};
public static final CopyListenerService.Companion Companion = new CopyListenerService.Companion((DefaultConstructorMarker)null);
@Nullable
public IBinder onBind(@Nullable Intent p0) {
return null;
}
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
ClipboardManager var10000 = this.getClipboardManager();
CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3 var10001 = new CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3;
Function0 var10003 = this.onPrimaryClipChangedListener;
if(this.onPrimaryClipChangedListener == null) {
Object var10002 = null;
} else {
var10001.<init>(var10003);
}
var10000.addPrimaryClipChangedListener((OnPrimaryClipChangedListener)var10001);
return 1;
}
public void onDestroy() {
super.onDestroy();
ClipboardManager var10000 = this.getClipboardManager();
CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3 var10001 = new CopyListenerServiceKt$sam$OnPrimaryClipChangedListener$15d0add3;
Function0 var10003 = this.onPrimaryClipChangedListener;
if(this.onPrimaryClipChangedListener == null) {
Object var10002 = null;
} else {
var10001.<init>(var10003);
}
var10000.removePrimaryClipChangedListener((OnPrimaryClipChangedListener)var10001);
}
@NotNull
public final ClipboardManager getClipboardManager() {
Lazy var1 = this.clipboardManager$delegate;
KProperty var3 = $$delegatedProperties[0];
return (ClipboardManager)var1.getValue();
}
@NotNull
public final Function0 getOnPrimaryClipChangedListener() {
return this.onPrimaryClipChangedListener;
}
@Metadata(
mv = {1, 1, 6},
bv = {1, 0, 1},
k = 1,
d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0002\b\u0003\b\u0086\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002R\u0014\u0010\u0003\u001a\u00020\u0004X\u0086D¢\u0006\b\n\u0000\u001a\u0004\b\u0005\u0010\u0006¨\u0006\u0007"},
d2 = {"Lcom/perqin/copyshare/CopyListenerService$Companion;", "", "()V", "TAG", "", "getTAG", "()Ljava/lang/String;", "production sources for module app"}
)
public static final class Companion {
@NotNull
public final String getTAG() {
return CopyListenerService.TAG;
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}