You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

能否仅用RxJava 2处理Android监听事件?含语音识别场景

Using RxJava 2 to Handle Android Voice Recognition Events

Absolutely, you can wrap Android's native voice recognition callbacks into RxJava 2 streams—no extra third-party libraries needed. The core idea is to convert the callback-based RecognitionListener into an Observable that emits events as the speech recognition process progresses. Here's a step-by-step, practical implementation:

1. Define a sealed class for typed recognition events

First, create a sealed class to encapsulate all possible events from the voice recognition workflow. This lets you handle different event types in a type-safe, clean way:

sealed class SpeechEvent {
    object Ready : SpeechEvent() // Recognizer is ready to listen
    class PartialResult(val text: String) : SpeechEvent() // Real-time partial recognition text
    class FinalResult(val texts: List<String>) : SpeechEvent() // Final recognition results
    class Error(val errorCode: Int) : SpeechEvent() // Recognition-specific error occurred
    object EndOfSpeech : SpeechEvent() // User stopped speaking
}

2. Build a custom Observable wrapper for SpeechRecognizer

Next, create an Observable that initializes the native SpeechRecognizer and forwards all RecognitionListener callbacks as SpeechEvent emissions:

fun createSpeechObservable(context: Context): Observable<SpeechEvent> {
    return Observable.create { emitter ->
        val speechRecognizer = SpeechRecognizer.createSpeechRecognizer(context)
        val recognitionListener = object : RecognitionListener {
            override fun onReadyForSpeech(params: Bundle?) {
                emitter.onNext(SpeechEvent.Ready)
            }

            override fun onBeginningOfSpeech() {}

            override fun onRmsChanged(rmsdB: Float) {}

            override fun onBufferReceived(buffer: ByteArray?) {}

            override fun onEndOfSpeech() {
                emitter.onNext(SpeechEvent.EndOfSpeech)
            }

            override fun onError(errorCode: Int) {
                emitter.onNext(SpeechEvent.Error(errorCode))
                // We don't call onComplete here so the stream can retry if needed
            }

            override fun onResults(results: Bundle?) {
                results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)?.let { texts ->
                    emitter.onNext(SpeechEvent.FinalResult(texts))
                }
                emitter.onComplete() // Close stream after final result is emitted
            }

            override fun onPartialResults(partialResults: Bundle?) {
                partialResults?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)?.firstOrNull()?.let { text ->
                    emitter.onNext(SpeechEvent.PartialResult(text))
                }
            }

            override fun onEvent(eventType: Int, params: Bundle?) {}
        }

        speechRecognizer.setRecognitionListener(recognitionListener)

        // Start listening as soon as the observable is subscribed
        val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
            putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
            putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault())
        }
        speechRecognizer.startListening(intent)

        // Clean up resources when the observer disposes the subscription
        emitter.setCancellable {
            speechRecognizer.stopListening()
            speechRecognizer.destroy()
        }
    }
}

3. Subscribe and handle events in your UI component

Finally, subscribe to the observable in your Activity/Fragment, making sure to manage the subscription lifecycle to avoid memory leaks:

class VoiceRecognitionActivity : AppCompatActivity() {
    private var disposable: Disposable? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_voice_recognition)

        // Check for RECORD_AUDIO permission first
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), 100)
            return
        }

        startVoiceRecognition()
    }

    private fun startVoiceRecognition() {
        disposable = createSpeechObservable(this)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(
                { event ->
                    when (event) {
                        is SpeechEvent.Ready -> Toast.makeText(this, "Listening...", Toast.LENGTH_SHORT).show()
                        is SpeechEvent.PartialResult -> updatePartialText(event.text)
                        is SpeechEvent.FinalResult -> handleFinalResults(event.texts)
                        is SpeechEvent.Error -> handleRecognitionError(event.errorCode)
                        is SpeechEvent.EndOfSpeech -> Toast.makeText(this, "Stopped listening", Toast.LENGTH_SHORT).show()
                    }
                },
                { error ->
                    Log.e("SpeechRx", "Unexpected error", error)
                    Toast.makeText(this, "Failed to start recognition", Toast.LENGTH_LONG).show()
                }
            )
    }

    private fun updatePartialText(text: String) {
        findViewById<TextView>(R.id.partial_text_view).text = text
    }

    private fun handleFinalResults(texts: List<String>) {
        findViewById<TextView>(R.id.final_text_view).text = texts.joinToString("\n")
    }

    private fun handleRecognitionError(errorCode: Int) {
        val errorMsg = when (errorCode) {
            SpeechRecognizer.ERROR_NO_MATCH -> "No speech detected"
            SpeechRecognizer.ERROR_SPEECH_TIMEOUT -> "Silence detected"
            SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS -> "Need recording permission"
            else -> "Recognition error: $errorCode"
        }
        Toast.makeText(this, errorMsg, Toast.LENGTH_LONG).show()
    }

    override fun onDestroy() {
        super.onDestroy()
        disposable?.dispose() // Clean up subscription to prevent memory leaks
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == 100 && grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
            startVoiceRecognition()
        } else {
            Toast.makeText(this, "Recording permission is required", Toast.LENGTH_LONG).show()
        }
    }
}

Key Tips

  • Lifecycle Safety: Always dispose subscriptions in onDestroy() (use CompositeDisposable if you have multiple streams) to avoid memory leaks.
  • Flexibility: You can expand the SpeechEvent class to include more events (like onRmsChanged for volume feedback) if your use case needs it.
  • RxJava Operators: Leverage RxJava's operators (like retry, filter, or map) to add logic like retrying on errors, filtering partial results, or transforming text outputs.

This approach keeps your code reactive, eliminates callback nesting, and lets you use all of RxJava's powerful features with native Android voice recognition.

内容的提问来源于stack exchange,提问作者Tima

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 06:50:56