能否仅用RxJava 2处理Android监听事件?含语音识别场景
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()(useCompositeDisposableif you have multiple streams) to avoid memory leaks. - Flexibility: You can expand the
SpeechEventclass to include more events (likeonRmsChangedfor volume feedback) if your use case needs it. - RxJava Operators: Leverage RxJava's operators (like
retry,filter, ormap) 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

