xDocxDoc
AI
前端
后端
iOS
Android
Flutter
AI
前端
后端
iOS
Android
Flutter
  • Android 面试合集

    • Android面试基础篇(一):基础架构与核心组件深度剖析
    • Android中高级面试(二):架构组件与协程实战精要
    • Android中高级面试(三):架构组件与性能优化实战
    • Android 面试(四): Kotlin 基础
    • Android面试(五):深入Kotlin协程
    • Android面试(六):深入Kotlin Flow
    • Android面试(七):Jetpack Compose 深度解析与高频考点
    • Android架构面试(八):从Jetpack到模块化设计
    • Android架构面试(九):Clean Architecture 终极指南
    • Android面试全栈指南:从Kotlin到Jetpack Compose

Android架构面试(九):Clean Architecture 终极指南

1.1 分层结构与依赖规则

Clean Architecture将Android应用分为三层:

  • Presentation Layer(表现层):处理UI逻辑(如Activity/Fragment/Compose),通过ViewModel与领域层交互
  • Domain Layer(领域层):封装业务逻辑与Use Cases,严格独立于Android框架(无Android SDK依赖)
  • Data Layer(数据层):管理数据源(数据库/API),通过Repository模式提供统一接口

依赖规则:内层不依赖外层 → 领域层不感知数据层实现细节,仅通过接口交互。

1.2 SOLID原则在架构中的应用

// 示例:依赖倒置原则(DIP)实现  
interface PaymentRepository { // 领域层定义抽象  
    suspend fun processPayment(amount: Double): Boolean  
}

class PaymentUseCase(  
    private val repo: PaymentRepository // 依赖抽象而非具体实现  
) {  
    suspend fun execute() = repo.processPayment(100.0)  
}

// 数据层实现接口(外层依赖内层)  
class PaymentRepositoryImpl : PaymentRepository {  
    override suspend fun processPayment(amount: Double): Boolean {  
        // 调用支付SDK(具体实现)  
    }  
}

代码注释:

  • PaymentRepository接口在领域层定义,确保业务逻辑与支付SDK解耦
  • PaymentRepositoryImpl在数据层实现,通过依赖注入(如Dagger/Hilt)提供给UseCase

1.3 与领域驱动设计(DDD)的关系

  • 领域模型:如User、Order实体类,包含核心业务属性与方法
  • 领域服务:跨实体的业务逻辑(如订单校验服务)
  • 界限上下文:模块化拆分业务边界(外卖拆分为订单/支付/用户服务)

🔧 二、分层实现详解与代码实战

2.1 领域层(Domain Layer)设计

2.1.1 Use Cases封装业务逻辑

class LoginUseCase(  
    private val authRepo: AuthRepository,  
    private val threadExecutor: ThreadExecutor // 线程调度抽象  
) {  
    suspend operator fun invoke(email: String, password: String): Result<Boolean> {  
        // 业务规则校验  
        if (!isValidEmail(email)) throw ValidationException("Invalid email")  
        return authRepo.login(email, password) // 调用仓库  
    }  
}

关键点:

  • UseCase命名规范:功能名 + UseCase(如LoginUseCase)
  • 每个UseCase仅处理单一业务逻辑

2.1.2 领域模型映射

// 数据层模型 → 领域模型转换  
data class ArticleEntity(val id: Int, val title: String, val content: String)  

data class Article(val id: Int, val title: String) // 领域模型  

class ArticleMapper : Mapper<Article, ArticleEntity> {  
    override fun mapToDomain(entity: ArticleEntity): Article {  
        return Article(entity.id, entity.title) // 仅保留核心字段  
    }  
}

优势: 隔离数据层细节,避免数据库结构污染业务逻辑

2.2 数据层(Data Layer)实现

2.2.1 Repository模式与数据源选择

class ArticleRepositoryImpl(  
    private val localSource: ArticleLocalDataSource,  
    private val remoteSource: ArticleRemoteDataSource  
) : ArticleRepository {  

    override fun getArticles(): Flow<List<Article>> {  
        return localSource.isCached().flatMapConcat { isCached ->  
            if (isCached) localSource.getArticles()   
            else remoteSource.getArticles().also { localSource.cache(it) }  
        }  
    }  
}

策略: 优先缓存 → 减少网络请求,提升响应速度

2.2.2 数据源动态切换

class DataStoreFactory(  
    private val cache: CacheDataStore,   
    private val remote: RemoteDataStore  
) {  
    fun getDataStore(isCacheValid: Boolean): DataStore {  
        return if (isCacheValid) cache else remote  
    }  
}

应用场景: 根据网络状态自动切换本地/云端数据源

2.3 表现层(Presentation Layer)状态管理

2.3.1 ViewModel + LiveData实现单向数据流

class ArticleViewModel(  
    private val getArticlesUseCase: GetArticlesUseCase  
) : ViewModel() {  

    private val _state = MutableLiveData<Resource<List<ArticleView>>>()  
    val state: LiveData<Resource<List<ArticleView>>> get() = _state  

    fun loadArticles() {  
        viewModelScope.launch {  
            _state.value = Resource.Loading()  
            try {  
                val articles = getArticlesUseCase.execute()  
                _state.value = Resource.Success(articles.map { it.toView() })  
            } catch (e: Exception) {  
                _state.value = Resource.Error(e.message)  
            }  
        }  
    }  
}

设计要点:

  • Resource封装加载/成功/错误状态
  • ArticleView为UI专用模型,避免暴露领域模型细节

2.3.2 MVI模式进阶实现

data class ArticleState(  
    val isLoading: Boolean = false,  
    val articles: List<ArticleView> = emptyList(),  
    val error: String? = null  
)  

class ArticleViewModel : ViewModel() {  
    private val _state = MutableStateFlow(ArticleState())  
    val state: StateFlow<ArticleState> = _state.asStateFlow()  

    fun dispatch(action: Action) {  
        when (action) {  
            is Action.Load -> loadArticles()  
        }  
    }  

    private fun loadArticles() { /* 更新_state */ }  
}

优势: 状态集中管理,避免分散LiveData导致的逻辑碎片化


🏢 三、企业级实践案例解析

3.1 外卖架构演进

阶段架构特点Clean Architecture对应点
O2O阶段(2012-2015)服务耦合度高未严格分层
平台化阶段(2018-至今)微服务独立部署模块化(订单/支付/用户服务)
数据层统一API网关Repository接口标准化

核心改进: 通过领域驱动设计划分界限上下文,团队并行开发

3.2 清晰架构设计

  • 端口与适配器模式:
    • 主适配器:处理用户输入(如点击事件)
    • 从适配器:实现外部服务(如支付SDK)
  • 控制反转:业务模块依赖接口而非实现 → 替换数据源无需修改领域层

💡 四、高频面试题深度剖析

4.1 理论类问题

Q1:为什么ViewModel能避免内存泄漏?
答:ViewModel不与Activity/Fragment绑定,而是关联ViewModelStore。当配置变更时,Activity重建但ViewModelStore保留,ViewModel实例未被销毁;当Activity永久销毁时,ViewModelStore清除并触发onCleared()释放资源。

Q2:如何确保领域层纯净?
答:三原则验证:

  1. 无Android SDK导入语句
  2. 所有外部依赖通过接口抽象(如ThreadExecutor替代CoroutineDispatcher)
  3. 单元测试可在JVM环境运行(无需Robolectric)

4.2 代码实战类问题

Q3:实现Token过期自动刷新

class AuthInterceptor(private val tokenRepo: TokenRepository) : Interceptor {  
    override fun intercept(chain: Interceptor.Chain): Response {  
        val request = chain.request()  
        if (tokenRepo.isTokenExpired()) {  
            synchronized(this) {  
                tokenRepo.refreshToken() // 同步刷新  
            }  
        }  
        return chain.proceed(request.newBuilder()  
            .header("Authorization", tokenRepo.getToken())  
            .build())  
    }  
}

关键点:

  • 同步块防止并发刷新
  • Token状态由领域层TokenRepository管理

4.3 架构设计类问题

Q4:如何设计跨模块数据共享?
方案:

  1. 共享ViewModel:
    // Fragment间共享  
    val sharedVM: SharedViewModel by activityViewModels()
  2. 事件总线(谨慎使用):
    interface EventBus {  
        fun publish(event: Event)  
        fun observe(owner: LifecycleOwner, observer: (Event) -> Unit)  
    }
  3. 单例仓库(推荐):
    class GlobalDataRepository @Inject constructor() {  
        private val _data = MutableStateFlow<Data?>(null)  
        val data: StateFlow<Data?> get() = _data.asStateFlow()  
    }

🧪 五、单元测试策略

5.1 领域层测试(纯JUnit)

class LoginUseCaseTest {  
    @Test  
    fun `invalid email should throw exception`() = runTest {  
        // 模拟Repository  
        val mockRepo = mockk<AuthRepository>()  
        val useCase = LoginUseCase(mockRepo)  

        // 验证异常  
        assertThrows<ValidationException> { useCase("invalid-email", "password") }  
    }  
}

工具链: JUnit 5 + MockK + Turbine(Flow测试)

5.2 UI层测试(Espresso + Hilt)

@HiltAndroidTest  
class LoginFragmentTest {  
    @get:Rule val hiltRule = HiltAndroidRule(this)  

    @Test  
    fun loginSuccess_shouldNavigateToHome() {  
        // 模拟ViewModel  
        val mockVM = mockk<LoginViewModel>()  
        every { mockVM.login(any(), any()) } returns flowOf(Result.Success(true))  

        // 启动Fragment  
        launchFragmentInHiltContainer<LoginFragment>()  

        // 操作UI并验证  
        onView(withId(R.id.btn_login)).perform(click())  
        onView(withId(R.id.home_layout)).check(matches(isDisplayed()))  
    }  
}

🔍 六、常见陷阱

陷阱现象后果解决方案
在UseCase中直接使用Context内存泄漏+无法单元测试通过接口抽象Context操作(如LocationProvider)
数据层模型传递到UI层UI逻辑污染领域层定义ViewData类并添加Mapper转换
ViewModel包含业务逻辑职责过重+难以复用将业务逻辑移至UseCase,ViewModel仅协调UI状态

🎯 总结

面试要点

  1. 分层价值

    • 可测试性:领域层独立于Android SDK(JUnit覆盖率>90%)
    • 团队协作:模块化拆分支持百人团队并行开发
    • 技术可持续性:替换框架(如Retrofit→Ktor)仅需修改数据层
  2. 面试回答公式

    理论阐述 → 代码示例 → 企业实践 → 个人经验

    示例回答:
    "ViewModel的生命周期管理通过ViewModelStore实现,在配置变更时保留实例(代码见4.1),借此解决订单页面数据丢失问题,我们在项目中结合SavedStateHandle处理进程重建..."

  3. 架构演进趋势

    • 模块化:从分层到组件化(独立交付支付/用户模块)
    • 响应式:Flow/Channels替代LiveData实现跨模块通信
    • 多平台:共享领域层(KMM跨Android/iOS)
最后更新: 2025/9/29 08:41
Prev
Android架构面试(八):从Jetpack到模块化设计
Next
Android面试全栈指南:从Kotlin到Jetpack Compose