24、扩展函数与高阶函数在依赖注入中的应用:Hilt的扩展函数、Kodein的DSL语法、自定义作用域

依赖注入(DI)在Android开发中已经成了标配。但说实话,很多人用Hilt或Kodein,只是照着文档抄,并不清楚背后的扩展函数和高阶函数到底起了什么作用。

今天我们就来扒一扒,这些DI框架是怎么利用Kotlin的语言特性,把依赖注入变得这么优雅的。

一、Hilt的扩展函数:@EntryPoint与Component的桥梁

Hilt是Google官方推荐的DI框架,基于Dagger。但Hilt比Dagger好用太多,原因之一就是它大量使用了扩展函数。

举个例子,当你想在非Android组件(比如一个普通的Java类)中获取依赖时,Hilt提供了EntryPoint机制。但每次都要写一大段样板代码,很烦。

Hilt的扩展函数帮你解决了这个问题。比如EntryPointAccessors系列函数:

// Hilt内部提供的扩展函数
inline fun <reified T> Context.getEntryPoint(): T {
    return EntryPointAccessors.fromActivity(this, T::class.java)
}

// 使用的时候
val myEntryPoint = context.getEntryPoint<MyEntryPoint>()
val service = myEntryPoint.getMyService()

你看,一个扩展函数就把类型推断和强转都封装好了。我当初在项目里重构老代码时,就靠这个把几十个Activity里的DI代码砍掉了一半。

核心思路:Hilt的扩展函数本质上是把Dagger的Component访问逻辑,包装成对开发者更友好的API。你不需要关心Component是怎么创建的,只需要调用一个扩展函数就行。

二、Kodein的DSL语法:高阶函数构建依赖图

Kodein是我个人比较喜欢的轻量级DI框架。它的DSL语法,说白了就是高阶函数的极致运用。

你看Kodein的典型用法:

val kodein = Kodein {
    bind<ApiService>() with singleton { RetrofitApiService() }
    bind<UserRepository>() with scoped(SingletonScope) { UserRepositoryImpl(instance()) }
    bind<LoginViewModel>() with factory { LoginViewModel(instance()) }
}

这个Kodein { ... }是什么?其实就是一个高阶函数。它的签名大概是这样的:

fun Kodein(block: KodeinBuilder.() -> Unit): Kodein

看到了吗?KodeinBuilder.() -> Unit就是一个带接收者的lambda。这个接收者KodeinBuilder里定义了bindsingletonfactory这些扩展函数。

我在项目中遇到过一个问题:团队里有人把singletonfactory搞混了,导致一个本该每次创建新实例的ViewModel变成了单例,结果数据一直不刷新。嗯,这里要注意:

  • singleton:整个应用生命周期内只创建一次实例
  • factory:每次请求都创建新实例
  • scoped:在某个作用域内保持单例

避坑指南:我曾经在Kodein的DSL里嵌套了多层作用域,结果调试依赖关系时差点崩溃。建议你保持DSL结构扁平化,不要超过两层嵌套。

三、自定义作用域:高阶函数控制生命周期

作用域是DI框架里最容易被忽视但又最重要的概念。说白了,作用域就是控制对象什么时候创建、什么时候销毁。

在Kodein中,自定义作用域的实现其实就是一个高阶函数:

class UserSessionScope : Scope {
    private val storage = mutableMapOf<ScopeKey, Any>()

    override fun get(key: ScopeKey): Any? = storage[key]

    override fun put(key: ScopeKey, item: Any) {
        storage[key] = item
    }

    override fun remove(key: ScopeKey) {
        storage.remove(key)
    }
}

// 使用自定义作用域
val userScope = UserSessionScope()
val kodein = Kodein {
    bind<UserData>() with scoped(userScope) { UserData() }
}

你想想看,这个scoped函数本身也是一个高阶函数。它接收一个Scope对象和一个创建实例的lambda,然后在内部管理这个实例的生命周期。

Hilt的作用域是通过注解实现的,比如@ActivityScoped@FragmentScoped。但Hilt的作用域是编译期确定的,而Kodein的作用域是运行时动态的。各有优劣。

注意:自定义作用域一定要处理好内存泄漏问题。我曾经在项目里写了一个Activity作用域,但忘记在onDestroy时清理,结果导致整个Activity被泄漏。血的教训。

四、扩展函数与高阶函数的组合拳

其实,不管是Hilt还是Kodein,它们都在做同一件事:用扩展函数简化API调用,用高阶函数构建DSL。

我总结了一个小规律:

框架 扩展函数用途 高阶函数用途
Hilt 简化EntryPoint访问、Component获取 @Module中的provider函数
Kodein bind、singleton、factory等DSL函数 KodeinBuilder的lambda构建
自定义 封装作用域管理逻辑 控制实例创建与销毁

说白了,扩展函数让你写更少的代码,高阶函数让你写更清晰的代码。两者结合,就是DI框架的优雅之源。

五、知识体系总览

下面这张图帮你理清本章的核心逻辑:

扩展函数与高阶函数在DI中的应用 Hilt扩展函数 EntryPointAccessors Component获取 类型推断封装 Kodein DSL 带接收者的lambda bind/singleton/factory 作用域管理 自定义作用域 Scope接口实现 生命周期控制 内存泄漏防范 核心思想 扩展函数简化API调用 + 高阶函数构建DSL = 优雅的依赖注入 实践建议:优先使用框架内置作用域,自定义作用域务必做好清理

这张图把三个核心模块的关系讲清楚了。Hilt和Kodein虽然实现方式不同,但底层都是靠扩展函数和高阶函数在支撑。自定义作用域则是你深入掌握DI的关键。

好了,这一章的内容就到这里。记住,扩展函数和高阶函数不是花架子,它们是真正能帮你写出干净代码的工具。下次你用Hilt或Kodein时,不妨想想背后那些优雅的扩展函数和高阶函数。


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