26、架构最佳实践:Clean Architecture、分层设计、依赖注入Hilt/Dagger、模块化

说实话,很多Android开发者做了两三年项目,代码能跑,但一遇到需求变更就头疼。我自己也经历过那种「改一个功能,崩三个页面」的至暗时刻。后来我慢慢意识到,问题不在技术,在架构。

这一章,我们就聊聊怎么把项目组织得清爽、可维护、好扩展。说白了,就是Clean Architecture、分层设计、依赖注入和模块化这四板斧。

为什么需要架构?

你想想看,一个App刚开始就几个页面,随便写写没问题。但业务一复杂,Activity里塞了几千行代码,ViewModel里全是网络请求和数据库操作……这时候你改一行代码都得提心吊胆。

架构的核心目的就两个:分离关注点控制依赖方向。让每一层只做自己该做的事,上层依赖下层,下层绝不依赖上层。

核心原则:依赖倒置原则(Dependency Inversion Principle)——高层模块不应该依赖低层模块,两者都应该依赖抽象。

Clean Architecture 分层设计

我习惯把项目分成三层:数据层领域层展示层。每一层都有自己的职责,互不越界。

1. 数据层(Data Layer)

负责数据的获取和存储。包括网络请求、本地数据库、SharedPreferences等。这一层是离「脏活」最近的,但也是最容易被替换的。

  • Repository:数据仓库,统一管理数据来源。我一般会写一个接口,然后实现类里决定是从网络拿还是从本地拿。
  • DataSource:具体的数据源,比如Room DAO、Retrofit Service。
  • DTO(Data Transfer Object):网络或数据库返回的原始数据模型,不与UI层直接耦合。

2. 领域层(Domain Layer)

这是整个架构的核心,不依赖任何Android框架。里面只放业务逻辑和用例(UseCase)。

  • UseCase:一个用例就是一个业务动作。比如「获取用户信息」、「保存笔记」。每个UseCase只做一件事。
  • Entity:业务实体,纯Kotlin数据类,不包含任何注解或框架依赖。
  • Repository接口:定义数据操作的抽象,具体实现在数据层。

我的习惯:领域层里只放接口和纯Kotlin代码。这样单元测试特别好写,不需要启动模拟器,跑得飞快。

3. 展示层(Presentation Layer)

负责UI展示和用户交互。包括Activity、Fragment、ViewModel、Compose等。

  • ViewModel:持有状态,调用UseCase。不直接操作数据库或网络。
  • UI State:用密封类或数据类表示页面的各种状态(加载中、成功、失败)。
  • Adapter / Composable:纯UI渲染,不包含业务逻辑。

嗯,这里要注意:展示层绝对不能直接依赖数据层的实现。ViewModel只能通过UseCase间接访问数据。

依赖注入:Hilt vs Dagger

依赖注入说白了就是「我要什么,你给我什么,我自己不去new」。我早期写代码都是手动new对象,后来项目大了,手动管理依赖简直要命。Hilt和Dagger就是来解决这个问题的。

特性 Dagger Hilt
上手难度 高,需要手动写Component、Module 低,注解驱动,内置Android组件支持
编译速度 较快 稍慢(因为做了更多封装)
官方推荐 是(Google官方推荐)
适用场景 大型项目,需要精细控制 大多数Android项目

我个人建议:新项目直接用Hilt。Dagger虽然灵活,但配置起来太啰嗦。我在一个老项目里用过Dagger,光Component就写了七八个,每次加个依赖都要改一堆文件。后来切到Hilt,清爽多了。

Hilt 基本用法

// 1. Application 类上添加 @HiltAndroidApp
@HiltAndroidApp
class MyApp : Application()

// 2. Activity 上添加 @AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var viewModel: MainViewModel
    // ...
}

// 3. ViewModel 使用 @HiltViewModel
@HiltViewModel
class MainViewModel @Inject constructor(
    private val getUserInfoUseCase: GetUserInfoUseCase
) : ViewModel() {
    // ...
}

// 4. 提供依赖的 Module
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
    @Provides
    @Singleton
    fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(
            context,
            AppDatabase::class.java,
            "my_database"
        ).build()
    }
}

避坑指南:我曾经在Module里忘记加@Singleton注解,结果每次注入都是新实例,导致数据库连接泄漏。后来排查了半天才发现。所以,单例对象一定要加@Singleton

模块化:分而治之

模块化就是把一个App拆成多个独立的模块。每个模块可以单独编译、单独测试。我参与过一个电商项目,光模块就分了十几个:common、network、database、user、product、order……

模块化的好处很明显:

  • 编译速度提升:改一个模块只编译那个模块,不用全量编译。
  • 团队协作:不同团队负责不同模块,互不干扰。
  • 复用性:common模块可以给多个App用。

常见的模块划分方式

  1. 按功能划分:user模块、product模块、order模块。每个模块包含自己的数据层、领域层和展示层。
  2. 按层级划分::data、:domain、:presentation。每个层级一个模块,功能模块依赖这些层级模块。
  3. 混合模式:先按层级分,再按功能分。比如 :data:user、:data:product、:domain:user、:domain:product。

我个人比较推荐按功能划分 + 公共层级模块。比如:

app/
├── common/          // 公共工具、基类
├── data/            // 数据层公共代码
├── domain/          // 领域层公共代码
├── feature/
│   ├── user/        // 用户功能模块
│   ├── product/     // 商品功能模块
│   └── order/       // 订单功能模块
└── app/             // 主工程,组装所有模块

架构核心逻辑图

下面这张图展示了Clean Architecture在Android中的典型分层和依赖方向。注意箭头方向——依赖永远指向内层。

数据层 (Data Layer) 领域层 (Domain Layer) 展示层 (Presentation Layer) Repository 实现 Room DAO / Retrofit UseCase Repository 接口 ViewModel Activity / Fragment 依赖方向 内层不依赖外层

依赖注入与模块化的结合

模块化之后,模块之间的依赖关系怎么管理?答案就是依赖注入。Hilt可以跨模块提供依赖,只要在对应模块里定义好Module和Provides方法。

举个例子:

// 在 :data 模块中定义数据库 Module
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(context, AppDatabase::class.java, "app.db").build()
    }
}

// 在 :domain 模块中定义 UseCase,依赖 Repository 接口
class GetUserUseCase @Inject constructor(
    private val userRepository: UserRepository
) {
    suspend operator fun invoke(userId: String): User {
        return userRepository.getUser(userId)
    }
}

// 在 :feature:user 模块中,ViewModel 直接注入 UseCase
@HiltViewModel
class UserViewModel @Inject constructor(
    private val getUserUseCase: GetUserUseCase
) : ViewModel() {
    // ...
}

小技巧:模块之间的依赖尽量用接口,不要直接依赖实现类。这样替换实现的时候,只需要改Module里的绑定,其他地方不用动。

总结一下

Clean Architecture、分层设计、依赖注入和模块化,这四样东西组合起来,就是一个能打的项目骨架。我这些年踩过的坑,十有八九都是因为架构没设计好。早期图省事,后期加倍还债。

嗯,最后说一句:架构不是银弹。小项目没必要硬套Clean Architecture,但一旦业务复杂起来,提前做好分层和模块化,绝对能让你少熬几个通宵。

核心要点回顾:

  • 数据层、领域层、展示层各司其职,依赖方向由外向内
  • 领域层是核心,不依赖任何框架
  • Hilt简化依赖注入,新项目首选
  • 模块化按功能划分,公共模块抽离
  • 依赖注入是模块间解耦的关键
公众号:蓝海资料掘金营,微信deep3321