9、DSL在依赖注入中的应用:Koin的DSL语法、自定义Koin Module、DSL构建依赖图、作用域与生命周期管理

依赖注入(DI)在Android开发中几乎是绕不开的话题。我最早接触DI是从Dagger开始的,说实话,那套注解处理器和APT编译时的代码生成,确实强大,但学习曲线也够陡的。后来Koin出现,我第一反应是「这玩意儿靠谱吗?没有编译时检查,不会出幺蛾子吧?」

结果用了一段时间后,我承认——Koin的DSL设计得太舒服了。它把依赖注入变成了一种「写配置」的感觉,而不是「写胶水代码」。今天我们就来聊聊Koin的DSL语法,以及怎么用它来构建清晰的依赖图。

Koin的DSL语法:从module开始

Koin的核心DSL就三个关键词:modulesinglefactory。你想想看,整个依赖注入的配置,说白了就是告诉框架「这个对象怎么创建、什么时候创建、存活多久」。

val appModule = module {
    // 单例:整个应用生命周期内只创建一次
    single { Retrofit.Builder()
        .baseUrl("https://api.example.com")
        .addConverterFactory(GsonConverterFactory.create())
        .build() }

    // 工厂:每次注入都创建新实例
    factory { UserRepository(get()) }

    // 带参数的工厂
    factory { (userId: String) -> UserSession(userId) }
}

我个人习惯把single用于无状态的服务类(比如网络客户端、数据库驱动),把factory用于有状态的业务对象。嗯,这里要注意:千万别把有状态的对象声明成single,否则多个页面共享同一个对象实例,数据一乱,排查起来够你喝一壶的。

自定义Koin Module:模块化你的依赖

项目大了以后,把所有依赖塞到一个module里,那画面太美我不敢看。我建议按功能模块拆分:

val networkModule = module {
    single { HttpClient() }
    single { ApiService(get()) }
}

val repositoryModule = module {
    single { UserRepository(get()) }
    single { ProductRepository(get()) }
}

val viewModelModule = module {
    factory { MainViewModel(get(), get()) }
    factory { DetailViewModel(get()) }
}

// 启动时加载
startKoin {
    modules(networkModule, repositoryModule, viewModelModule)
}

这样做的好处很明显:每个模块职责清晰,测试时可以只加载需要的模块。我曾经在一个大型项目中,把模块拆到了20多个,每个模块对应一个feature,后来新人接手时,看模块列表就能大概知道项目结构。

DSL构建依赖图:可视化你的依赖关系

依赖图这东西,光靠脑补是不行的。我习惯在项目里画一张图,把核心依赖的上下游关系理清楚。下面这张图是我常用的依赖图结构:

Koin 依赖图结构 HttpClient single(全局单例) ApiService single(依赖HttpClient) UserRepository single(依赖ApiService) ProductRepository single(依赖ApiService) MainViewModel factory(依赖两个Repository) DetailViewModel factory(依赖ProductRepository) UserSession scope(用户登录期间存活) CartManager scope(购物车生命周期) 网络层(single) 仓库层(single) ViewModel层(factory) 作用域层(scope)

从这张图可以清楚看到:依赖是单向流动的。ViewModel依赖Repository,Repository依赖ApiService,ApiService依赖HttpClient。这种分层结构,说白了就是让每一层只关心自己的事。

作用域与生命周期管理

Koin的作用域(Scope)是我觉得最实用的功能之一。它解决了「某个对象在特定时间段内存活」的问题。比如用户登录后的会话信息、购物车数据,这些对象既不是全局单例,也不是每次创建新实例——它们应该在用户登录时创建,退出登录时销毁。

// 定义作用域
val userScopeModule = module {
    scope(named("userSession")) {
        scoped { UserSession(get()) }
        scoped { CartManager() }
    }
}

// 在Activity/Fragment中使用
class MainActivity : AppCompatActivity() {
    // 创建作用域实例
    private val userScope = getKoin().createScope("user_001", named("userSession"))

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 从作用域中获取实例
        val session: UserSession = userScope.get()
        val cart: CartManager = userScope.get()
    }

    override fun onDestroy() {
        super.onDestroy()
        // 退出时关闭作用域,释放所有scoped实例
        userScope.close()
    }
}
⚠️ 避坑指南
我曾经在一个项目中,忘记在Activity销毁时调用scope.close(),结果导致内存泄漏——每次进入页面都创建一个新的作用域,旧的作用域一直没释放。排查了半天才发现是作用域没关。所以,作用域的生命周期一定要和它的宿主绑定,比如在onDestroyonCleared中关闭。

DSL构建依赖图的进阶技巧

当依赖图变得复杂时,我推荐用模块依赖来组织。Koin允许你在一个module中引用另一个module的依赖,只要它们都被加载了:

val featureModule = module {
    // 直接引用其他module中定义的依赖
    factory { FeatureViewModel(get(), get()) }
}

// 启动时确保所有模块都被加载
startKoin {
    modules(
        networkModule,   // 提供HttpClient、ApiService
        repositoryModule, // 提供UserRepository、ProductRepository
        featureModule     // 使用上面两个模块的依赖
    )
}

这样做的好处是:模块之间解耦。featureModule不需要知道HttpClient是怎么创建的,它只需要知道「我能拿到UserRepository就够了」。这种设计思想,说白了就是「依赖倒置」——高层模块不依赖低层模块的具体实现,只依赖抽象。

💡 个人经验
我习惯在项目里建一个di包,里面按功能分文件:NetworkModule.ktDatabaseModule.ktRepositoryModule.ktViewModelModule.kt。每个文件只放一个module定义,文件顶部用注释说明这个module提供了哪些依赖。这样新人来了,看文件名就能知道依赖在哪定义。

总结一下

Koin的DSL让依赖注入变得很「Kotlin」——简洁、可读、类型安全。它不像Dagger那样需要写一堆注解和Component接口,也不像手动DI那样需要自己管理对象创建。说白了,Koin就是在「自动」和「手动」之间找到了一个平衡点。

我个人觉得,对于中小型项目,Koin是性价比最高的DI方案。它的DSL语法学习成本低,作用域管理灵活,而且和Android的生命周期结合得很好。如果你还没试过Koin,我建议从一个小模块开始,感受一下DSL写依赖注入的爽快感。


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