一、为什么多媒体开发需要扩展函数与高阶函数?
说实话,Android 的多媒体 API 一直让我又爱又恨。爱的是功能强大,恨的是调用起来太啰嗦。拿 MediaPlayer 来说,你要先创建实例,再设置数据源,然后准备,最后启动——这还没算上错误处理和资源释放。一套流程下来,代码量直接翻倍。
我在项目中遇到过好几次这样的场景:产品经理说「加个音效播放」,我打开代码一看,好家伙,MediaPlayer 的样板代码写了七八遍。那时候我就想,能不能用 Kotlin 的扩展函数把这些重复劳动干掉?
后来我发现,扩展函数 + 高阶函数这个组合,简直就是为多媒体量身定做的。你想想看:
- 扩展函数:给 MediaPlayer、CameraX 这些类「加私货」,不破坏原有结构
- 高阶函数:把回调、配置逻辑包装成 Lambda,调用方只需要关心「做什么」
说白了,就是让 API 更符合人的直觉。下面我带你一个个看。
二、MediaPlayer 的扩展函数:让播放器更好用
2.1 基础封装:一行代码播放
先看最常用的场景——播放一个音频文件。原生写法是这样的:
val mediaPlayer = MediaPlayer()
mediaPlayer.setDataSource(context, uri)
mediaPlayer.setOnPreparedListener { it.start() }
mediaPlayer.prepareAsync()
每次写这四行,你不烦我都烦。用扩展函数改一下:
fun MediaPlayer.play(context: Context, uri: Uri, loop: Boolean = false) {
setDataSource(context, uri)
isLooping = loop
setOnPreparedListener { it.start() }
prepareAsync()
}
调用的时候:
MediaPlayer().play(context, uri, loop = true)
嗯,清爽多了。我个人习惯把这类扩展函数放在一个叫 MediaPlayerExt.kt 的文件里,按功能分组。
2.2 高阶函数处理生命周期
MediaPlayer 有个烦人的点——资源释放。Activity 销毁了你还得手动 release。用高阶函数可以这样:
inline fun MediaPlayer.safePlay(
crossinline onError: (String) -> Unit = {},
block: MediaPlayer.() -> Unit
) {
try {
block()
setOnErrorListener { _, what, extra ->
onError("Error: $what, $extra")
true
}
} catch (e: Exception) {
onError(e.message ?: "Unknown error")
}
}
fun MediaPlayer.releaseOnDestroy(lifecycleOwner: LifecycleOwner) {
lifecycleOwner.lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
release()
}
})
}
调用方只需要关心播放逻辑:
val player = MediaPlayer()
player.safePlay {
play(context, uri)
}
player.releaseOnDestroy(this) // Activity/Fragment 销毁时自动释放
三、CameraX 的 DSL 配置:拍照也能写声明式代码
3.1 传统配置 vs DSL 配置
CameraX 本身已经很简洁了,但配置起来还是有点绕。比如你要设置预览、拍照、分析三个用例:
val preview = Preview.Builder()
.setTargetAspectRatio(AspectRatio.RATIO_16_9)
.build()
val imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture, imageAnalysis)
代码本身没问题,但读起来不够直观。用 DSL 改造一下:
class CameraXDSL(private val lifecycleOwner: LifecycleOwner) {
private val useCases = mutableListOf<UseCase>()
private var selector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
fun preview(block: Preview.Builder.() -> Unit) {
useCases.add(Preview.Builder().apply(block).build())
}
fun imageCapture(block: ImageCapture.Builder.() -> Unit) {
useCases.add(ImageCapture.Builder().apply(block).build())
}
fun imageAnalysis(block: ImageAnalysis.Builder.() -> Unit) {
useCases.add(ImageAnalysis.Builder().apply(block).build())
}
fun cameraSelector(selector: CameraSelector) {
this.selector = selector
}
fun bind(provider: ProcessCameraProvider) {
provider.bindToLifecycle(lifecycleOwner, selector, *useCases.toTypedArray())
}
}
fun ProcessCameraProvider.bindDSL(
lifecycleOwner: LifecycleOwner,
block: CameraXDSL.() -> Unit
) {
val dsl = CameraXDSL(lifecycleOwner).apply(block)
dsl.bind(this)
}
调用时就像在写配置清单:
cameraProvider.bindDSL(this) {
cameraSelector(CameraSelector.DEFAULT_BACK_CAMERA)
preview {
setTargetAspectRatio(AspectRatio.RATIO_16_9)
}
imageCapture {
setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
}
imageAnalysis {
setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
}
}
四、音频录制的简化:高阶函数封装状态机
4.1 录制状态管理
音频录制涉及多个状态:空闲、录制中、暂停、错误。用高阶函数可以把状态转换封装成链式调用:
class AudioRecorder(private val filePath: String) {
enum class State { IDLE, RECORDING, PAUSED, ERROR }
private var state: State = State.IDLE
private var mediaRecorder: MediaRecorder? = null
fun start(): AudioRecorder {
if (state != State.IDLE) throw IllegalStateException("Not idle")
mediaRecorder = MediaRecorder().apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
setOutputFile(filePath)
prepare()
start()
}
state = State.RECORDING
return this
}
fun pause(): AudioRecorder {
if (state != State.RECORDING) throw IllegalStateException("Not recording")
mediaRecorder?.pause()
state = State.PAUSED
return this
}
fun resume(): AudioRecorder {
if (state != State.PAUSED) throw IllegalStateException("Not paused")
mediaRecorder?.resume()
state = State.RECORDING
return this
}
fun stop(): AudioRecorder {
mediaRecorder?.apply {
stop()
release()
}
mediaRecorder = null
state = State.IDLE
return this
}
inline fun onStateChange(crossinline block: (State) -> Unit): AudioRecorder {
// 实际项目中可以用回调或 Flow 实现
block(state)
return this
}
}
链式调用看起来像流水线:
val recorder = AudioRecorder("/sdcard/audio.aac")
recorder
.onStateChange { state -> Log.d("Recorder", "State: $state") }
.start()
.pause()
.resume()
.stop()
4.2 高阶函数处理权限与异常
录制前要检查权限,录制中可能出错。用高阶函数包装一下:
fun AudioRecorder.withPermission(
context: Context,
onGranted: AudioRecorder.() -> Unit,
onDenied: () -> Unit = {}
) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO)
== PackageManager.PERMISSION_GRANTED
) {
onGranted()
} else {
ActivityCompat.requestPermissions(
context as Activity,
arrayOf(Manifest.permission.RECORD_AUDIO),
REQUEST_CODE
)
// 实际项目中需要处理 onRequestPermissionsResult
onDenied()
}
}
调用时:
recorder.withPermission(this,
onGranted = {
start()
},
onDenied = {
Toast.makeText(this, "需要录音权限", Toast.LENGTH_SHORT).show()
}
)
五、知识体系总览
下面这张图总结了本章的核心逻辑:
六、总结
这一章我们聊了三件事:
- MediaPlayer 扩展函数:把重复的播放流程封装成一行,用高阶函数处理错误和生命周期
- CameraX DSL 配置:用 Lambda 接收者把 Builder 调用变成声明式配置,代码可读性提升一个档次
- 音频录制简化:用状态机 + 链式调用管理录制流程,用高阶函数处理权限和异常
说白了,Kotlin 的这些特性不是为了炫技,而是实实在在解决 Android 开发中的痛点。我在项目中用这套方案重构了多媒体模块,代码量减少了 40%,bug 率也明显下降。你下次写 MediaPlayer 或 CameraX 的时候,不妨试试这些模式。
公众号:蓝海资料掘金营,微信deep3321