Android架构面试题:MVP/MVVM/MVI都分不清,架构师跟你没关系

3 阅读18分钟

Android架构面试题:MVP/MVVM/MVI都分不清,架构师跟你没关系

1. MVP vs MVVM vs MVI:不是选哪个,是什么场景用哪个?

核心回答

先泼盆冷水:上来就问"你们用什么架构"的面试官,其实自己也没想清楚要面什么。

这三种架构不是什么升级换代的关系,而是解决不同问题的工具

  • MVP:适合小型项目、快速交付。Model和View完全解耦,但Presenter太重,View和Presenter容易形成双向依赖
  • MVVM:适合中大型项目、团队协作。Google官方推荐,数据绑定让View和ViewModel单向依赖
  • MVI:适合复杂状态、响应式UI。单向数据流,状态不可变,适合状态多且交互复杂的场景

原理/代码

MVP:  View ←→ Presenter ←→ Model
      ↑                      ↓
      └────── 接口回调 ───────┘

MVVM: View ←─── 绑定 ──── ViewModel ←─── Repository ←─── Model
                     (单向依赖)

MVI:  Intent → Model → View (单向数据流)
           ↑
           └── 状态不可变,只产生新状态

举个小例子:登录按钮点击

// MVP
class LoginPresenter {
    fun onLoginClick(username: String, password: String) {
        // 直接调用View
        view.showLoading()
        // ...业务逻辑
        view.navigateToHome()
    }
}

// MVVM
class LoginViewModel : ViewModel() {
    private val _loginState = MutableLiveData<LoginState>()
    val loginState: LiveData<LoginState> = _loginState
    
    fun onLoginClick(username: String, password: String) {
        viewModelScope.launch {
            _loginState.value = LoginState.Loading
            val result = repository.login(username, password)
            _loginState.value = result.fold(
                onSuccess = { LoginState.Success },
                onFailure = { LoginState.Error(it.message) }
            )
        }
    }
}

// MVI
sealed class LoginIntent {
    data class Login(val username: String, val password: String) : LoginIntent()
}

sealed class LoginState {
    object Idle : LoginState()
    object Loading : LoginState()
    data class Success(val user: User) : LoginState()
    data class Error(val message: String) : LoginState()
}

// Intent处理器
fun reduce(intent: LoginIntent): LoginState {
    return when(intent) {
        is LoginIntent.Login -> LoginState.Loading
        // 真正的MVI会在这里做状态转换,而不是直接赋值
    }
}

Android实战场景

选MVP:外包项目、短期活动页、demo。你要的是快速出活,不是可维护性。Facebook早期App用的变体MVP。

选MVVM:90%的业务项目。Jetpack全家桶天然适配,LiveData/DataBinding/StateFlow随便选。Google推荐不是没道理的。

选MVI:金融App、聊天App、状态机复杂的场景。微信团队在技术分享中提过类似思路——把用户操作抽象成Intent,状态驱动UI。

面试加分点

面试官想听的不只是"用什么",而是"为什么换"或"为什么选这个"。

加分回答:

  1. 我们项目从MVP迁到MVVM:Presenter测试困难,View和Presenter通过接口耦合,改一个要改两个文件
  2. MVVM里ViewModel不是万能的:屏幕旋转等配置变更ViewModel会重建,要配合SavedStateHandle
  3. MVI的代价:状态类膨胀,每个页面可能几十个状态case。但好处是状态可追溯,出问题能回放

2. MVVM中ViewModel和View的通信方式有哪些?LiveData vs StateFlow vs SharedFlow怎么选?

核心回答

这个问题本质是问:你怎么理解Android的"响应式"?

三种方式各有各的坑,没有银弹:

  • LiveData: lifecycle-aware,页面可见才接收数据,但粘性事件问题让人头疼
  • StateFlow:协程原生,非粘性,每次collect拿到最新值,但要注意生命周期
  • SharedFlow:事件总线,适合一次性事件,但配置复杂

原理/代码

// LiveData 粘性事件问题
// 假设Activity重建:
// 1. ViewModel发出 LoginSuccess
// 2. Activity start但还没observe
// 3. Activity resume,observe拿到LoginSuccess(粘性!)
// 4. 用户可能看到闪一下的Loading然后又跳转

// 解决方案1:LiveData配合 SingleLiveEvent(已废弃,不推荐)
class SingleLiveEvent<T> : MutableLiveData<T>() {
    private val pending = AtomicBoolean(false)
    
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        super.observe(owner) { t ->
            if (pending.compareAndSet(true, false)) {
                observer.onChanged(t)
            }
        }
    }
}

// 解决方案2:StateFlow(非粘性)
class LoginViewModel : ViewModel() {
    private val _loginState = MutableStateFlow<LoginState>(LoginState.Idle)
    val loginState: StateFlow<LoginState> = _loginState.asStateFlow()
    
    // 一次性事件用 SharedFlow
    private val _navigationEvent = MutableSharedFlow<NavigationEvent>()
    val navigationEvent = _navigationEvent.asSharedFlow()
}

// View侧
lifecycleScope.launch {
    viewModel.loginState.collect { state ->
        // 非粘性,只有真正collect时才收到
        when(state) {
            is LoginState.Success -> showSuccess(state.user)
            is LoginState.Error -> showError(state.message)
            LoginState.Loading -> showLoading()
        }
    }
}

// SharedFlow订阅(一次性事件)
lifecycleScope.launch {
    viewModel.navigationEvent.collect { event ->
        when(event) {
            is NavigationEvent.ToHome -> navController.navigate(R.id.home)
            is NavigationEvent.ToProfile -> navController.navigate(R.id.profile)
        }
    }
}

Android实战场景

普通数据(UI状态)用 StateFlow

data class UserListState(
    val users: List<User> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null
)
val state = MutableStateFlow(UserListState())

一次性事件用 SharedFlow

private val _toastMessage = MutableSharedFlow<String>()
val toastMessage: SharedFlow<String> = _toastMessage

// 发送
_toastMessage.emit("保存成功")

// 或用 tryEmit(不等待)
_toastMessage.tryEmit("保存成功") // 返回Boolean

为什么SharedFlow适合一次性事件

  • 新订阅者不会收到之前的消息
  • 可以配置replay参数决定要不要缓存
  • 支持多播(多个View可以同时订阅)

面试加分点

  1. 说清楚粘性事件的坑:很多新手不知道LiveData默认是粘性的,导致页面重建后收到意外数据

  2. 知道replay参数的用法

    // replay = 0:新订阅者什么都收不到
    // replay = 1:新订阅者收到最新一条
    MutableSharedFlow<Int>(replay = 1)
    
  3. 性能考虑:StateFlow比LiveData轻量,因为它不需要处理Lifecycle Owner

3. 一次性事件怎么处理?为什么LiveData不适合做事件?

核心回答

这是面试高频坑。先搞清楚概念:

状态 vs 事件

  • 状态:UI当前的样子,比如"正在加载"、"用户列表"——应该持久
  • 事件:通知View做一次性的操作,比如"跳转"、"弹Toast"——只执行一次

LiveData的粘性问题:旧值会被保留,新订阅者会立刻收到之前的值。这对状态没问题,但对事件是灾难。

原理/代码

// 场景:用户登录成功后跳转Home
// 用户在Home页按返回键回到Login页,然后配置改变导致Activity重建
// 这时新Activity会收到之前的LoginSuccess事件,触发又一次跳转

// 问题复现
class LoginViewModel : ViewModel() {
    private val _loginSuccess = MutableLiveData<Boolean>()
    val loginSuccess: LiveData<Boolean> = _loginSuccess
}

// 解决1:Event Wrapper(老方案,不推荐但要懂)
class Event<out T>(private val content: T) {
    private var hasBeenHandled = false
    
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) null
        else {
            hasBeenHandled = true
            content
        }
    }
    
    fun peekContent(): T = content // 如果你非要拿值
}

// 使用
_loginSuccess = Event(true)

// View
viewModel.loginSuccess.observe(this) { event ->
    event.getContentIfNotHandled()?.let { success ->
        if (success) navigateToHome()
    }
}

// 解决2:SharedFlow(推荐)
class LoginViewModel : ViewModel() {
    private val _loginSuccess = MutableSharedFlow<Unit>()
    val loginSuccess: SharedFlow<Unit> = _loginSuccess
    
    fun login() {
        viewModelScope.launch {
            _loginSuccess.emit(Unit)
        }
    }
}

// View
lifecycleScope.launch {
    viewModel.loginSuccess.collect {
        navigateToHome()
        // 不需要Event包装,因为SharedFlow默认非粘性
    }
}

// 解决3:Channel(SharedFlow的兄弟)
// Channel是热流,有缓冲但默认容量为0,必须有消费者
private val _loginSuccess = Channel<Unit>(Channel.BUFFERED)
val loginSuccess = _loginSuccess.receiveAsFlow()

Android实战场景

什么时候用SharedFlow

  • 导航事件(跳转、返回)
  • Toast/Snackbar消息
  • Dialog显示
  • 任何"触发一次"的交互

什么时候用StateFlow

  • UI状态(loading、error、data)
  • 表单内容
  • 任何需要保持的数据

为什么不用Event LiveData了

  1. 需要手动处理hasBeenHandled,很容易漏掉
  2. 多订阅者时处理逻辑复杂
  3. Google官方推荐StateFlow/SharedFlow,LiveData定位是UI状态持有者

面试加分点

如果你能说出"状态和事件的本质区别"和"粘性事件的原理",面试官会觉得你对Android生命周期理解到位。

加分项:

  1. 说清楚LiveData为什么是粘性的:设计初衷是为了解决Configuration Change后数据丢失问题
  2. 理解Channel vs SharedFlow:Channel用于协程间通信,容量为0时是 suspend 的;SharedFlow是多订阅者场景
  3. 小心repeatOnLifecycle:配合Flow使用时,页面不可见时停止collect,不会收到事件

4. 模块化 vs 组件化:有什么区别?怎么拆?模块间通信怎么做?

核心回答

先纠正一个常见误区:模块化和组件化不是一回事

表格

模块化组件化
目标代码解耦、职责分离独立编译、独立运行
粒度按业务/层级划分按功能/UI划分
结果还是一个App可能拆出多个App
典型例子feature-user、feature-orderlogin-component、video-player

简单说:模块化是代码组织方式,组件化是发布方式

原理/代码

groovy

// settings.gradle.kts
include(":app")
include(":features:home")
include(":features:profile")
include(":features:order")
include(":modules:network")
include(":modules:common")
include(":components:login") // 这个可以独立运行

// gradle.properties
# 组件化开关
isComponentEnable=true

// app/build.gradle.kts
plugins {
    id("com.android.application")
}

android {
    namespace = "com.example.app"
    
    defaultConfig {
        applicationId = "com.example.app"
    }
}

dependencies {
    implementation(project(":features:home"))
    implementation(project(":features:profile"))
    implementation(project(":modules:common"))
    
    // 组件化时排除login,自己独立运行
    if (!isComponentEnable) {
        implementation(project(":components:login"))
    }
}

// components:login/build.gradle.kts
plugins {
    id("com.android.application") // 可以运行成独立App
    // 或 id("com.android.library") // 作为库
}

模块间通信:Router方案

// 1. 定义接口(基于协议而非实现)
interface HomeNavigator {
    fun navigateToProfile(userId: String)
    fun navigateToOrder(orderId: String)
}

// 2. App模块提供Router实现
class AppRouter : HomeNavigator {
    private val navController: NavController
    
    override fun navigateToProfile(userId: String) {
        navController.navigate(R.id.profileFragment, bundleOf("userId" to userId))
    }
}

// 3. Home模块依赖接口,不依赖实现
class HomeViewModel(
    private val navigator: HomeNavigator, // 注入
    private val userRepository: UserRepository
) {
    fun onUserClick(userId: String) {
        // 不直接依赖Profile模块
        navigator.navigateToProfile(userId)
    }
}

// 4. 手动注入或用Hilt
@Module
@InstallIn(SingletonComponent::class)
object NavigatorModule {
    @Provides
    fun provideHomeNavigator(router: AppRouter): HomeNavigator = router
}

Android实战场景

按什么维度拆

  1. 按业务域:user、order、product、payment
  2. 按层级:network、database、common、ui-components
  3. 按团队边界:哪个团队负责哪个模块

拆分的信号

  • 两个模块同时改同一个文件 → 该拆了
  • 编译时间超过5分钟 → 该拆了
  • 新人上手要改3个模块才能加一个功能 → 该拆了

不要过早拆分:很多小项目强行组件化,维护成本反而更高。MVP/MVVM都没跑清楚之前,别折腾模块化。

面试加分点

  1. 提到Arouter或AutoRoute:阿里的Arouter是组件化路由的事实标准

  2. 说出模块化的坑

    • R文件冲突(每个模块有自己R)
    • 资源名冲突
    • 循环依赖
    • 构建速度不见得更快(没有合理拆分的话)
  3. 了解动态加载:组件化终极形态是App Bundle、插件化。提到"宿主+插件"架构

5. Clean Architecture在Android中怎么落地?UseCase层到底要不要?

核心回答

Clean Architecture是好东西,但Android落地有代价。先说结论:

UseCase不是必须的,但分层思想是必须的

很多项目加了一层UseCase,结果只是:

class GetUserUseCase(private val repo: UserRepository) {
    suspend operator fun invoke(userId: String): User = repo.getUser(userId)
}

这叫套壳,不叫Clean Architecture。

原理/代码

Clean Architecture 分层(Android版)

┌─────────────────────────────────────┐
│           Presentation              │  ViewModel、Activity、Fragment
│    (UI层 / 状态驱动 / 响应式)         │
└─────────────────┬───────────────────┘
                  │
┌─────────────────▼───────────────────┐
│             Domain                  │  UseCase、Entity、业务规则
│    (纯Kotlin / 无Android依赖 / 可测试)│
└─────────────────┬───────────────────┘
                  │
┌─────────────────▼───────────────────┐
│              Data                   │  Repository实现、数据源、网络、DB
│    (外部世界 / 实现细节 / Framework)  │
└─────────────────────────────────────┘

UseCase的真正价值组合业务逻辑

// 场景:下单前要验证用户、验证库存、计算价格、创建订单

// 不用UseCase:全部塞ViewModel
class OrderViewModel {
    suspend fun createOrder(productId: String) {
        val user = userRepo.getUser()
        if (!userRepo.validateUser(user)) throw AuthException()
        
        val stock = inventoryRepo.getStock(productId)
        if (stock < 1) throw OutOfStockException()
        
        val price = priceCalculator.calculate(user, productId)
        orderRepo.create(...)
    }
}

// 用UseCase:每个步骤一个UseCase,可复用、可测试
class ValidateUserUseCase(private val userRepo: UserRepository) {
    suspend operator fun invoke(): User {
        val user = userRepo.getCurrentUser()
        if (!userRepo.isValid(user)) throw AuthException()
        return user
    }
}

class CheckStockUseCase(private val inventoryRepo: InventoryRepository) {
    suspend operator fun invoke(productId: String): Int {
        val stock = inventoryRepo.getStock(productId)
        if (stock < 1) throw OutOfStockException()
        return stock
    }
}

class CalculatePriceUseCase(private val priceRepo: PriceRepository) {
    suspend operator fun invoke(user: User, productId: String): BigDecimal {
        return priceRepo.calculate(user, productId)
    }
}

// ViewModel只编排
class OrderViewModel(
    private val validateUser: ValidateUserUseCase,
    private val checkStock: CheckStockUseCase,
    private val calculatePrice: CalculatePriceUseCase,
    private val createOrder: CreateOrderUseCase
) {
    val orderState = MutableStateFlow<OrderState>(OrderState.Idle)
    
    fun createOrder(productId: String) {
        viewModelScope.launch {
            orderState.value = OrderState.Loading
            try {
                val user = validateUser()
                checkStock(productId)
                val price = calculatePrice(user, productId)
                createOrder(productId, price)
                orderState.value = OrderState.Success
            } catch (e: Exception) {
                orderState.value = OrderState.Error(e.message ?: "Unknown")
            }
        }
    }
}

Android实战场景

什么时候需要UseCase

  • 业务逻辑复杂,多个步骤有复用需求
  • 需要单元测试,但不想mock太多Repository
  • 团队多人协作,需要清晰边界

什么时候不需要

  • 单个Repository调用就能搞定
  • 业务简单,ViewModel不臃肿
  • 项目小,快速迭代优先

Android落地建议

domain/
├── model/           # 领域实体
├── repository/      # 仓储接口(抽象,不依赖具体实现)
├── usecase/         # 用例
└── exception/       # 业务异常

data/
├── repository/      # 仓储实现
├── remote/          # 远程数据源
├── local/           # 本地数据源
└── mapper/          # 数据映射

面试加分点

  1. 说出Domain层为什么是纯Kotlin:不依赖Android才能做单元测试,才能被其他平台复用(Kotlin Multiplatform)

  2. 理解Dependency Rule:内层不依赖外层,外层依赖内层

  3. 知道反模式

    • 在UseCase里直接写Android代码
    • UseCase只是调用Repository(套壳)
    • 跨层依赖(Data直接引用Presentation)

6. Repository模式的正确写法:网络+本地+缓存的策略

核心回答

Repository不是"数据访问层",而是数据来源的决策者

常见错误:

  1. 直接暴露Network/DB的接口
  2. 没有任何缓存策略,每次都请求网络
  3. 缓存策略混乱,优先级不清楚

正确思路:以用户为中心,给用户最快的数据,同时保证数据新鲜

原理/代码

interface UserRepository {
    suspend fun getUser(id: String): Result<User>
    suspend fun refreshUser(id: String): Result<User>  // 强制刷新
}

// 实现:多层缓存 + 网络 + 本地
class UserRepositoryImpl(
    private val network: UserApi,
    private val local: UserDao,
    private val cache: MemoryCache  // LRU cache
) : UserRepository {
    
    // 优先级:内存 → 磁盘 → 网络
    // 写策略:网络 → 内存 + 磁盘
    
    override suspend fun getUser(id: String): Result<User> {
        // 1. 先查内存缓存(毫秒级)
        cache.get(id)?.let { return Result.success(it) }
        
        // 2. 查本地数据库
        val localUser = local.getUser(id)
        if (localUser != null) {
            cache.put(id, localUser)  // 回填内存
            // 后台刷新,不阻塞
            refreshInBackground(id)
            return Result.success(localUser)
        }
        
        // 3. 请求网络
        return fetchFromNetwork(id)
    }
    
    override suspend fun refreshUser(id: String): Result<User> {
        // 强制刷新:跳过缓存,直接网络
        return fetchFromNetwork(id)
    }
    
    private suspend fun fetchFromNetwork(id: String): Result<User> {
        return try {
            val user = network.getUser(id)
            // 写入本地和缓存
            local.insertUser(user)
            cache.put(id, user)
            Result.success(user)
        } catch (e: Exception) {
            // 网络失败时,返回本地数据(如果有)
            local.getUser(id)?.let { 
                Result.success(it) 
            } ?: Result.failure(e)
        }
    }
    
    private fun refreshInBackground(id: String) {
        // 使用WorkManager或其他机制后台刷新
        // 不阻塞主流程
    }
}

// Memory Cache 实现
class MemoryCache(private val maxSize: Int = 100) : LinkedHashMap<String, User>(maxSize, 0.75f, true) {
    override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, User>?): Boolean {
        return size > maxSize
    }
}

Android实战场景

常见的缓存策略

表格

策略适用场景实现难度
Cache-Aside大多数场景,读多写少
Read-Through需要统一的数据加载入口
Write-Through需要强一致性,写操作频繁
Write-Behind需要高写入性能,可以容忍短暂不一致

Cache-Aside(最常用)

读:先缓存 → 没有则读数据库 → 没有则读网络
写:更新网络 → 更新数据库

Android特有考虑

  • 网络质量差时,本地缓存是救命稻草
  • DiskLruCache/Room 适合大数据缓存
  • 内存缓存注意配置变更(Activity重建后丢失)

面试加分点

  1. 说出Room + Retrofit + Coroutines的配合:Room作为单一数据源,Repository是决策者

  2. 理解数据新鲜度:聊天列表需要实时,但设置页面可以缓存5分钟

  3. 知道缓存失效策略

    • TTL(Time To Live)
    • 主动失效(推送通知、主动刷新)
    • 版本号控制

7. 依赖注入:Hilt vs Koin vs 手动注入,怎么选?

核心回答

先问自己:为什么需要依赖注入?

不是为了"看起来高级",而是为了:

  1. 解耦:类不直接依赖具体实现
  2. 可测试:Mock依赖轻松替换
  3. 生命周期管理:Android有Activity、ViewModel等生命周期

选型建议

  • Hilt:大型项目、团队协作、Google官方支持
  • Koin:中小型项目、快速开发、不想用注解
  • 手动注入:极小项目、教学目的

原理/代码

// 手动注入(最原始,但也是基础)
class UserRepository(private val api: UserApi) { }

// 手动创建对象
class MainActivity : AppCompatActivity() {
    private val api = UserApi()
    private val repo = UserRepository(api)
    private val vm = MainViewModel(repo)
}

// 问题:依赖多了会爆炸,而且难以测试

// Koin(函数式DSL)
val appModule = module {
    // 单例
    single { UserApi() }
    single { UserRepository(get()) }
    single { GetUserUseCase(get()) }
    
    // ViewModel要传Activity Context的话
    viewModel { MainViewModel(get()) }
}

class MainActivity : AppCompatActivity() {
    private val vm: MainViewModel by viewModel()
    // 搞定!
}

// Hilt(编译时注解)
@HiltAndroidApp
class MyApp : Application()

@AndroidEntryPoint  // 自动生成DI代码
class MainActivity : AppCompatActivity() {
    @Inject lateinit var api: UserApi
    @Inject lateinit var vm: MainViewModel
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 注入已完成
    }
}

// ViewModel注入
class MainViewModel @Inject constructor(
    private val getUser: GetUserUseCase
) : ViewModel()

// Module定义
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideApi(retrofit: Retrofit): UserApi = retrofit.create()
}

Android实战场景

什么时候选Hilt

  • 项目大,依赖关系复杂
  • 需要多模块,每个模块自己提供依赖
  • 团队成员多,需要统一规范
  • 需要编译时检查,运行时零开销

什么时候选Koin

  • 小团队、个人项目
  • 不喜欢注解、喜欢DSL风格
  • 快速出MVP
  • 项目不复杂,不需要编译时代码生成

什么时候手动

  • 学习DI原理
  • 项目极小(就2-3个类)
  • 不想引入额外依赖

面试加分点

  1. 理解编译时 vs 运行时:Hilt在编译时生成代码,运行时无反射开销;Koin在运行时构建图谱
  2. 说出Hilt的局限性:编译时间长、代码膨胀、灵活性不如Dagger
  3. 提到其他方案:Dagger2(复杂但最灵活)、manual DI库(如Injekt)

8. Android的状态管理:状态提升、状态下沉、状态容器怎么选?

核心回答

这三种方式对应三种复杂度级别:

表格

方式复杂度适用场景
State Hoisting(状态提升)单页面、简单组件
State Down(状态下沉)父子组件通信
State Container(状态容器)复杂状态、跨页面

记住原则:状态应该被拥有它所需最少信息的组件持有。

原理/代码

// 1. 状态提升(最常用)
// 好:状态共享,兄弟组件能通信
@Composable
fun ParentScreen() {
    var text by remember { mutableStateOf("") }
    
    Column {
        // 子组件可以读写text
        InputField(text, { text = it })
        DisplayText(text)
    }
}

// 2. 状态下沉(Props Drilling的反模式,应该避免)
// 差:状态在根节点,往下层层传
@Composable
fun App() {
    var globalState by remember { mutableStateOf(...) }
    // 传5层,每层都要声明这个参数
    Level1(state = globalState, onUpdate = { globalState = it })
}

// 3. 状态容器(用ViewModel或State hoisting到合适层级)
// 好:状态归类,逻辑归位

// 场景:购物车页面
data class CartState(
    val items: List<CartItem> = emptyList(),
    val totalPrice: BigDecimal = BigDecimal.ZERO,
    val isCheckoutLoading: Boolean = false,
    val checkoutError: String? = null,
    val selectedItems: Set<String> = emptySet()
)

class CartViewModel @Inject constructor(
    private val cartRepo: CartRepository
) : ViewModel() {
    
    private val _state = MutableStateFlow(CartState())
    val state: StateFlow<CartState> = _state.asStateFlow()
    
    fun selectItem(itemId: String) {
        _state.update { current ->
            val newSelection = if (itemId in current.selectedItems) {
                current.selectedItems - itemId
            } else {
                current.selectedItems + itemId
            }
            current.copy(selectedItems = newSelection)
        }
    }
    
    fun checkout() {
        viewModelScope.launch {
            _state.update { it.copy(isCheckoutLoading = true, checkoutError = null) }
            try {
                val selectedItems = _state.value.items.filter { it.id in _state.value.selectedItems }
                cartRepo.checkout(selectedItems)
                _state.update { it.copy(isCheckoutLoading = false) }
            } catch (e: Exception) {
                _state.update { it.copy(isCheckoutLoading = false, checkoutError = e.message) }
            }
        }
    }
}

Android实战场景

什么时候用哪种

  1. 单页面简单状态:用ViewModel + StateFlow

    // 比如一个设置开关
    var notificationsEnabled by remember { mutableStateOf(true) }
    
  2. 需要跨组件共享

    // 两个子组件需要共享同一个状态
    @Composable
    fun SharedStateContainer() {
        var sharedState by remember { mutableStateOf(...) }
        ChildA(state = sharedState, onChange = { sharedState = it })
        ChildB(state = sharedState, onChange = { sharedState = it })
    }
    
  3. 全局状态:用State Container(App级ViewModel或Hilt Singleton)

    kotlin

    // 用户登录状态,全局共享
    @Singleton
    class UserSession @Inject constructor() {
        private val _isLoggedIn = MutableStateFlow(false)
        val isLoggedIn: StateFlow<Boolean> = _isLoggedIn.asStateFlow()
    }
    

面试加分点

  1. 说出Jetpack Compose的State Hoisting模式:状态提升到父组件,让子组件更可复用
  2. 理解一次性事件不提升:Toast/导航用SharedFlow,不放在State里
  3. 性能考虑:状态太大导致recompose范围大,用derivedStateOf优化

9. 多模块项目的Gradle构建优化:怎么加快编译速度?

核心回答

说个扎心的事实:大部分项目的编译慢,不是Gradle的问题,是你代码的问题。

但Gradle配置也有优化空间。

原理/代码

// gradle.properties
# JVM参数
org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

# 并行构建
org.gradle.parallel=true

# 配置缓存
org.gradle.configuration-cache=true  // Gradle 7.0+

# 按需配置
org.gradle.configure-on-demand=true

# 缓存
org.gradle.caching=true

# 非传递性依赖
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=true
android.nonFinalResIds=true
// app/build.gradle.kts
android {
    // 增量编译
    compileOptions {
        isIncremental = true
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    
    kotlinOptions {
        jvmTarget = "17"
        // 增量编译
        freeCompilerArgs += listOf("-Xjsr305=strict")
    }
    
    // 构建缓存
    buildFeatures {
        buildConfig = true
    }
}

// 分离模块的依赖配置
dependencies {
    // 只在debug时需要的
    debugImplementation("com.facebook.stetho:stetho:1.6.0")
    
    // 只在测试时需要的
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
}
// 禁用不必要的模块构建
// settings.gradle.kts
include(":features:legacy")  // 老代码,不常改

// 在需要的模块里引用
dependencies {
    if (project.hasProperty("includeLegacy")) {
        implementation(project(":features:legacy"))
    }
}

分模块构建

# 只构建指定模块
./gradlew :features:home:assembleDebug

# 依赖检查
./gradlew :app:dependencies --configuration debugRuntimeClasspath

Android实战场景

常见的编译慢原因

  1. Kotlin增量编译失败:clean后再build就好了,但治标不治本
  2. databinding/kapt:巨慢,能用ksp就用ksp
  3. 太多模块:每个模块都是独立项目,初始化开销大
  4. 依赖图太深api vs implementation 用错

优化清单

// 1. kapt → ksp(快3-5倍)
plugins {
    id("com.google.devtools.ksp")
}

// 2. 减少kapt使用
// Room、Hilt都有ksp支持
dependencies {
    ksp("androidx.room:room-compiler:2.6.0")
    ksp("com.google.dagger:hilt-compiler:2.48")
}

// 3. 合理使用api/implementation
// implementation:不暴露依赖,传给下游
// api:暴露依赖,下游可以直接用

面试加分点

  1. 提到Remote Build Cache:团队共享构建缓存
  2. 说出Gradle版本的重要性:8.x比7.x快很多
  3. 理解Kotlin Daemonkotlin.daemon.jvmargs=-Xmx2g

10. 如何设计一个可测试的Android架构?Unit Test和UI Test怎么写?

核心回答

测试不是"额外工作",是"架构设计正确与否的验证器"。

一个难以测试的架构,通常意味着设计有问题

可测试性原则

  1. 依赖抽象,不依赖具体实现
  2. 副作用(IO、网络)放到边界层
  3. ViewModel里不要有Android代码

原理/代码

// 1. Unit Test:测试ViewModel
class LoginViewModelTest {
    private lateinit var viewModel: LoginViewModel
    private lateinit var fakeUserRepository: FakeUserRepository
    
    @Before
    fun setup() {
        fakeUserRepository = FakeUserRepository()
        viewModel = LoginViewModel(fakeUserRepository)
    }
    
    @Test
    fun `login success should emit success state`() = runTest {
        // Given
        fakeUserRepository.setFakeUser(User("test", "123"))
        
        // When
        viewModel.login("test", "123")
        
        // Then
        assertEquals(
            LoginState.Success,
            viewModel.loginState.value
        )
    }
    
    @Test
    fun `login with wrong password should emit error`() = runTest {
        // Given
        fakeUserRepository.setFakeUser(User("test", "123"))
        
        // When
        viewModel.login("test", "wrong")
        
        // Then
        val state = viewModel.loginState.value
        assertTrue(state is LoginState.Error)
    }
}

// Fake Repository实现
class FakeUserRepository : UserRepository {
    private var fakeUser: User? = null
    
    fun setFakeUser(user: User) {
        fakeUser = user
    }
    
    override suspend fun login(username: String, password: String): Result<User> {
        return if (fakeUser?.password == password) {
            Result.success(fakeUser!!)
        } else {
            Result.failure(Exception("Invalid credentials"))
        }
    }
}
// 2. UI Test(Espresso)
@HiltAndroidTest
class LoginFragmentTest {
    @Inject
    lateinit var hiltRule: HiltAndroidRule
    
    @Test
    fun `login button click should show loading then navigate`() {
        // 准备数据
        testDispatcher.testScheduler.advanceTimeBy(1000)
        
        onView(withId(R.id.usernameEditText))
            .perform(typeText("testuser"), closeSoftKeyboard())
        
        onView(withId(R.id.passwordEditText))
            .perform(typeText("password123"), closeSoftKeyboard())
        
        onView(withId(R.id.loginButton))
            .perform(click())
        
        // 验证Loading显示
        onView(withId(R.id.progressBar))
            .check(matches(isDisplayed()))
        
        // 等待网络请求完成(使用IdlingResource或Fake)
        testDispatcher.testScheduler.advanceUntilIdle()
        
        // 验证跳转
        intended(hasComponent(HomeActivity::class.java))
    }
}
// 3. Fake数据层实现
class FakeUserRepositoryImpl @Inject constructor() : UserRepository {
    private var shouldFail = false
    private var delayMs = 0L
    
    fun setShouldFail(fail: Boolean) {
        shouldFail = fail
    }
    
    fun setDelay(ms: Long) {
        delayMs = ms
    }
    
    override suspend fun login(username: String, password: String): Result<User> {
        delay(delayMs)
        return if (shouldFail) {
            Result.failure(Exception("Network error"))
        } else {
            Result.success(User(username, "token123"))
        }
    }
}

Android实战场景

测试金字塔

        △ UI Test
       △ △ △    (少,E2E,慢)
     △ △ △ △ △
   △ △ △ △ △ △ △  Unit Test
 △ △ △ △ △ △ △ △ △ (多,单元,快)

Android测试工具链

  • Unit Test:JUnit4 + MockK/F Mockito + Turbine(Flow测试)
  • Instrumented Test:Espresso / Compose UI Test
  • Fake:生产环境和测试环境的数据隔离

常见问题

  1. ViewModel有Android依赖

    kotlin

    // 差
    class MyViewModel(val context: Context) { }
    
    // 好:抽象出来
    class MyViewModel(val stringProvider: StringProvider) { }
    interface StringProvider { ... }
    
  2. Repository有真实现

    kotlin

    // 差:依赖Retrofit
    class UserRepository(private val api: UserApi)
    
    // 好:接口隔离
    interface UserRepository { suspend fun getUser() }
    // 测试时用FakeUserRepository
    

面试加分点

  1. 说出Test Double的类型:Dummy、Fake、Stub、Mock、Spy,以及什么时候用什么
  2. 提到Coroutine Test:用runTest而不是runBlocking,因为后者会阻塞线程
  3. 理解UI Test的复杂性:UI测试慢、不稳定,但能验证真实用户体验

总结

架构题考的不是你知道几个模式,而是:

  1. 理解力:能说清楚每个模式的适用场景和trade-off
  2. 实战经验:踩过坑,知道什么时候该用什么
  3. 工程思维:架构是为团队服务的,不是炫技

核心心法

  • 没有银弹,只有取舍
  • 先跑通,再优化
  • 可测试性是架构好坏的试金石