Kotlinでリスナーとしてラムダを使用しないでください

こんにちは、Habr! Alex GherschonによるKotlinのリスナーとしてラムダを使用しないという記事の翻訳を紹介します



翻訳者から :Kotlinは非常に強力な言語であり、コードをより簡潔かつ迅速に書くことができます。 しかし、最近、言語の良い面を説明する記事が多すぎて、落とし穴について語っています。これは、言語が初心者にとってブラックボックスである新しいデザインをもたらすためです。 この記事は翻訳版であり、ラムダをAndroidのリスナーとして使用する方法について説明しています。 結局、言語を変更してもプラットフォームの仕様が消えないため、著者が踏んだのと同じレーキを踏まないようにするのが役立ちます。



Kotlinで書いた最初のアプリケーションでこの問題に出くわしました。



エントリー



ポッドキャストリスニングアプリでAudioFocusを使用しています。 ユーザーがエピソードを聴きたい場合、 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など)を停止してエピソードを再生するオーディオフォーカスを要求できるようになりました。



オーディオフォーカスの解放も機能しているように見えました。 AudioManagerクラスのabandonAudioFocusメソッドを呼び出すと、結果としてAUDIOFOCUS_REQUEST_GRANTEDが得られました。



 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
      
      





ただし、オーディオフォーカスを再度要求するとすぐに、すぐにそれを失い、 AUDIOFOCUS_LOSSイベントを取得します



 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
      
      





なぜ要求されたらすぐにそれを失うのですか? 何が起こっているの?



舞台裏



問題を理解するための最良のツールは、 Kotlin Bytecode Bytecode Viewerです。



画像



画像



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





次に、その使用方法を確認しましょう。 RequestAudioFocusメソッド:



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





AbandonAudioFocusメソッド:



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





実際には、次のことが発生します:lambda / Function1はonCreate()で初期化されますが、関数としてSAMとして渡すたびに、リスナーインターフェイスを実装するクラスの新しいインスタンスにラップされます。つまり、2つのインスタンスが作成されますリスナーとAudioManager API、abandonAudioFocus()を呼び出すときに削除できません。以前に作成され、 requestAudioFocus()を呼び出すときに使用されるリスナー 元のリスナーは削除されないため、 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変数はリスナーの同じインスタンスを参照するようになりました。これは、 AudioManagerクラスのrequestAudioFocusおよびabandonAudioFocusメソッドに正しく渡されます。 いいね!



コード例



生成されたバイトコードを見て、 GitHubのこのリポジトリで個人的問題を確認できます。



結論(しかし完全ではない)



大きな力には大きな責任が伴います。 リスナーの匿名内部クラスの代わりにラムダを使用しないでください。 私は重要な教訓を学びました。あなたもそれから利益を得ることを願っています。



追記



読者の一人がコメントで指摘したように(ありがとう、Pavlo!)次のようにラムダを宣言でき、すべてが正しく動作します:



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





追記説明



lateinitのせいですか?



一部の読者は、問題はlateinit修飾子を使用したリスナー宣言にあると主張しています。 これがlateinitかどうかを確認するには、この修飾子の有無にかかわらずラムダを実装して、結果を見てみましょう。



これが何であるかを思い出させるために、これら2つのラムダのコードを次に示します。



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







同じ問題にはlateinitがないことがわかるので、この修飾子を非難することはできません。



おすすめの方法



問題を解決するには、匿名の内部クラスを使用することをお勧めします。



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





匿名クラスは目的のインターフェイスを実装し、単一のインスタンスがあります(ここにはラムダがないため、コンパイラはSAM変換を行う必要はありません)。 いいね!



最善の方法



最も簡潔な方法は、ラムダを宣言し、ドキュメントで変換メソッドと呼ばれるものを使用することです。



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





これは、これがSAMを変換するときに使用する型であることをコンパイラに伝えます。 結果の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);
      
      





結論(今では完全に)



Roman DawydkinSlackで著しく発言したように:



一度使用した場合にのみ、ラムダをリスナーとして使用できます


ラムダが機能的なスタイルで、またはコールバック関数として使用される場合、問題はありません。 問題は、Observerパターンで同じインスタンスを予期するJava記述されたAPIでリスナーとして使用される場合にのみ表示されます。 APIがKotlinで記述されている場合、 SAM変換は行われないため、問題はありません。 いつか、API全体がそのようになるでしょう!



このトピックが誰にとっても非常に明確になったことを願っています。



校正についてはRhaquel Gherschonに、この記事に対するコメントについてはChristophe Beylsに感謝します!



やった!



翻訳者から :これは落とし穴の1つにすぎません。 別の例は、RxJava + SAM + Kotlinの束の中の間違ったブラケットです



All Articles