こんにちは、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 {
次に、その使用方法を確認しましょう。
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) ラムダは、インターフェイスを実装するクラス(SAM変換)にラップされていますが、変換されたクラスへの参照を所有していないため、問題があります。
lateinitなし(onAudioFocusChangeListener2) 同じ問題には
lateinitがないことがわかるので、この修飾子を非難することはできません。
おすすめの方法
問題を解決するには、匿名の内部クラスを使用することをお勧めします。
private val onAudioFocusChangeListener3: AudioManager.OnAudioFocusChangeListener = object : AudioManager.OnAudioFocusChangeListener { override fun onAudioFocusChange(focusChange: Int) { Log.d(TAG, "In onAudioFocusChangeListener2 focus changed to = $focusChange")
これは、Javaでは次のように変換されます。
匿名クラスは目的のインターフェイスを実装し、単一のインスタンスがあります(ここにはラムダがないため、コンパイラは
SAM変換を行う必要はありません)。 いいね!
最善の方法
最も簡潔な方法は、ラムダを宣言し、ドキュメント
で変換メソッドと呼ば
れるものを使用することです。
private val onAudioFocusChangeListener4 = AudioManager.OnAudioFocusChangeListener { focusChange: Int -> Log.d(TAG, "In onAudioFocusChangeListener3 focus changed to = $focusChange")
これは、これが
SAMを変換するときに使用する型であることをコンパイラに伝えます。 結果のJavaコード:
結論(今では完全に)
Roman Dawydkinが
Slackで著しく発言したように:
一度使用した場合にのみ、ラムダをリスナーとして使用できます
ラムダが機能的なスタイルで、またはコールバック関数として使用される場合、問題はありません。 問題は、Observerパターンで同じインスタンスを予期する
Javaで
記述された
APIでリスナーとして使用される場合にのみ表示されます。 APIがKotlinで記述されている場合、
SAM変換は行われないため、問題はありません。 いつか、API全体がそのようになるでしょう!
このトピックが誰にとっても非常に明確になったことを願っています。
校正についてはRhaquel Gherschonに、この記事に対するコメントについては
Christophe Beylsに感謝します!
やった!
翻訳者から :これは落とし穴の1つにすぎません。 別の例は
、RxJava + SAM + Kotlinの束の中の間違ったブラケットです