一、为什么多媒体开发需要扩展函数与高阶函数?

说实话,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 销毁时自动释放
我的经验: 曾经有个项目因为忘记 release MediaPlayer,导致内存泄漏,用户反馈播放几次后 app 就卡死。后来我强制团队所有 MediaPlayer 必须绑定 Lifecycle,再没出过问题。

三、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)
    }
}
核心思想: DSL 的本质是用 Lambda 接收者(receiver)把嵌套的 Builder 调用「拍平」。你不需要记住 Builder 的创建顺序,只需要关注「我要什么」。

四、音频录制的简化:高阶函数封装状态机

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()
    }
)
注意: 我曾经在项目中直接用 MediaRecorder 的 start() 方法,没检查权限,结果在 Android 6.0 以上的设备上直接崩溃。后来我强制所有录制操作必须经过权限检查,再也没出过类似问题。

五、知识体系总览

下面这张图总结了本章的核心逻辑:

扩展函数与高阶函数在多媒体中的应用 MediaPlayer 扩展 • play() 一行播放 • safePlay() 安全封装 • releaseOnDestroy() • 高阶函数处理错误 • 链式调用 • 生命周期绑定 CameraX DSL 配置 • bindDSL() 声明式 • preview {} 配置 • imageCapture {} • imageAnalysis {} • Lambda 接收者 • Builder 拍平 音频录制简化 • 状态机封装 • start() / pause() • resume() / stop() • onStateChange() • withPermission() • 链式调用 核心思想:用扩展函数消除样板代码,用高阶函数抽象行为逻辑

六、总结

这一章我们聊了三件事:

  • MediaPlayer 扩展函数:把重复的播放流程封装成一行,用高阶函数处理错误和生命周期
  • CameraX DSL 配置:用 Lambda 接收者把 Builder 调用变成声明式配置,代码可读性提升一个档次
  • 音频录制简化:用状态机 + 链式调用管理录制流程,用高阶函数处理权限和异常

说白了,Kotlin 的这些特性不是为了炫技,而是实实在在解决 Android 开发中的痛点。我在项目中用这套方案重构了多媒体模块,代码量减少了 40%,bug 率也明显下降。你下次写 MediaPlayer 或 CameraX 的时候,不妨试试这些模式。

一个小建议: 扩展函数不要写太多参数,超过 3 个就用 Builder 或 DSL。否则调用方记不住参数顺序,反而降低了可读性。

公众号:蓝海资料掘金营,微信deep3321